Files
llm-wiki/raw/branch-notes/feature-keycloak-single-ec2-no-google.md

45 KiB
Raw Permalink Blame History

title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, branch, parent_branch, related_projects, tags, created, target_merge, status_label, contract_packet_sha256
title source_type status id kind project work_item inherits refines overrides depends_on contract_packet branch parent_branch related_projects tags created target_merge status_label contract_packet_sha256
branch / feature-keycloak-single-ec2-no-google (P3A Single EC2 — client + backend + keycloak 동거, no Google) branch-note raw BR-KEYCLOAK-CHILD-FE8F0749 branch-child keycloak-patterns-overview WI-KEYCLOAK-PATTERNS-OVERVIEW-020
DEC-KEYCLOAK-PATTERNS-OVERVIEW-AUTH-TAXONOMY-001@1
1 feature-keycloak-single-ec2-no-google feature-keycloak-patterns
keycloak-patterns
branch
keycloak-patterns
auth
oauth2
oidc
docker
2026-05-25 in-progress 930562ffd6f26bab1c938d08a2d2403cdc3627a5b242ffabe05057a32f792e17

branch: feature-keycloak-single-ec2-no-google (P3A Single EC2, no Google)

Layer: raw/branch-notes/raw/branch-notes/feature-keycloak-patterns의 WI020 child branch. 본 패턴은 6개 패턴 중 유일하게 vanilla JS로 실 구현되는 케이스. 나머지 5개(P1A/P1B/P2A/P2B/P3B)는 문서/다이어그램까지만. 축 재편 (2026-07-14): hub raw/project-notes/keycloak-patterns-overview §2.3 에서 P3A → AP1 (Browser-based OAuth Client = SPA-direct + Resource Server) + cross-cutting 배포=single-EC2 (실 구현 base) 로 re-map 됨. hub 고정 결정 F5(§5)가 "E2E 실 구현 배포 = single-EC2 docker-compose 1벌" 로 확정 → 본 노트는 4 패턴(AP1~AP4) 전체가 얹히는 물리 배포 base 이다(그 위 실행 단계는 6개 자식이 owner — §구현 가이드 §3). 본문의 "P3A" 프레이밍은 Phase 0 표기이며, rename/re-parent 은 hub §2.3·§8 대로 wiki-doc-author mode=migrate 로 점진 수행(§진행 중 메모 AXIS_DRIFT).

부모 (필수)

raw/branch-notes/feature-keycloak-patterns

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: project governance hub가 AP1~AP4 taxonomy와 child progress index를 유지한다

상속한 프로젝트 결정

Decision Ref Project Summary Branch Application Source
DEC-KEYCLOAK-PATTERNS-OVERVIEW-AUTH-TAXONOMY-001@1 canonical 분류축은 AP1~AP4 인증 통합 아키텍처와 cross-cutting 변형이다 single-EC2를 AP1~AP4가 공유하는 deployment cross-cutting 변형으로 분류한다 raw/project-notes/keycloak-patterns-overview

브랜치 지역 결정

기존 branch-local 결정은 아래 ## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.

Decision ID Decision Relation Supporting Claims Status

선언한 예외

Override ID Overrides Reason Approval Status

없음.

목표

단일 EC2 호스트에 client (vanilla JS SPA via nginx static) + backend (Spring Boot) + Keycloak 세 컴포넌트를 동거시킨 상태에서, OIDC Authorization Code + PKCE 흐름이 실제로 어떻게 동작하는지 코드 레벨로 학습. 토큰 교환 흐름은 P2A와 동일(Browser → Keycloak → Backend Resource Server JWT validation). 차이는 네트워크 토폴로지KC_HOSTNAME 함정.

면접에서 "OIDC 전체 lifecycle을 직접 구현해 봤다 → access/refresh/ID token 차이, PKCE 필요 이유, JWT issuer 검증 메커니즘을 코드로 설명 가능"이 목표.

범위

포함 범위

  • 단일 EC2 배포 토폴로지 확정 (본 branch 고유 소유) — nginx(SPA static) + Spring Boot(Resource Server) + Keycloak + PostgreSQL 를 단일 호스트 docker-compose 로 동거시키는 물리 경계·포트 노출·localhost trust 확정(D3, hub F5). hub 재편 후 4 패턴(AP1~AP4) E2E 가 모두 이 배포 base 위에 얹힌다.
  • KC_HOSTNAME iss 함정의 배포측 정의 — 단일 host 에서 browser 와 backend 가 같은 issuer hostname 을 봐야 하는 이유·해결 축 확정(D3). 재현·해결 절차 자체는 자식 raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch 소관.
  • HTTPS-less 학습 경계 확정 (D1) — 학습 환경은 HTTP, prod 진입 시 Caddy / nginx + Let's Encrypt 로 termination 추가.
  • vanilla JS 로 OIDC lifecycle 을 실제로 구현하는 유일 케이스 — 6 실 구현 단계(자식)로 분해(§Cluster), 각 단계가 planned → 구현 후 locally-verified 승급.
  • 문서 산출물 — 컴포넌트 토폴로지 · 토큰 교환 sequence · 단일 호스트 특이점(KC_HOSTNAME/redirect_uri) · 장단점 (모두 현재 planned/documented-only).

제외 범위

의도적으로 제외한 것. 면접 등에서 "이건 범위에 없었습니다"라고 답할 근거.

근거 (필수, 최소 1개+)

본 sub-branch의 P3A (Single EC2, 실 구현 대상) 채택 근거. 상세 대안 비교는 §외부 근거 / 대안 조사 참조.

Source 정당화하는 결정
raw/official-docs/keycloak-server-containers-docker Keycloak Docker container 공식 — docker-compose 채택 근거
raw/official-docs/keycloak-hostname-configuration Keycloak hostname guide — KC_HOSTNAME 설정 근거
raw/official-docs/keycloak-getting-started-docker Docker quickstart — 단일 host 학습 구성 근거
raw/official-docs/spring-security-resource-server-jwt Spring Security Resource Server JWT 검증 — backend Resource Server 근거
raw/official-docs/oauth2-pkce-rfc-7636 RFC 7636 PKCE — public client 필수 PKCE 근거
raw/official-docs/oidc-client-ts-library oidc-client-ts — vanilla JS OIDC client 라이브러리 선택 근거

외부 근거 / 대안 조사 (2026-05-25 — P3A Single EC2)

본 sub-branch의 단일 EC2 (client + backend + keycloak 동거) + Authorization Code + PKCE 채택에 대한 외부 source. P2A를 단일 호스트로 압축한 형태 + 단일 호스트 고유 함정.

  • 채택 결정 (Single Host Docker Compose + PKCE + Keycloak KC_HOSTNAME):
  • 검토한 대안:
    • 대안 1: Cluster 배치 (P2A) — Kubernetes 또는 ECS로 분리 배치. 장: prod-like / 단: 학습 friction 큼 (네트워크 / DNS / cert 모두 관리). 비교 sub-branch: raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.
    • 대안 2: Edge ForwardAuth on Single Host — nginx + oauth2-proxy + Keycloak + backend 모두 단일 host. 장: P1A 학습 가능 / 단: vanilla JS SPA 흐름 학습이 주 목적과 어긋남 (proxy가 인증 처리, SPA는 token 모름).
    • 대안 3: BFF on Single Host — Spring Boot이 Keycloak token holder. 장: 보안 우월 (token이 SPA에 없음) / 단: vanilla JS의 OIDC 학습 목적과 어긋남 (SPA가 session cookie만 사용).
    • 대안 4: Direct host (no Docker) — Keycloak + Spring Boot + nginx를 EC2에 직접 설치. 장: docker overhead 0 / 단: 환경 reset 어려움, 학습 반복 비용 큼.
    • 대안 5: 사전 빌드 이미지 (Keycloak Helm + Spring Boot Image) — managed Keycloak. 학습 단계엔 과함.
  • 비교 핵심: 단일 EC2 + Docker Compose는 OIDC 전체 lifecycle을 가장 작은 surface로 학습. KC_HOSTNAME 미설정 시 iss claim mismatch가 단일 host의 가장 흔한 함정 — browser는 localhost:8080, backend는 Docker internal keycloak:8080 보면서 JWT issuer가 mismatch → JWT validation 실패. 해결: KC_HOSTNAME=localhost + KC_HTTP_ENABLED=true 명시. redirect_uri는 localhost vs 127.0.0.1 한 글자만 달라도 mismatch → Keycloak client 등록 시 두 URI 모두 등록 또는 사용 일관화. PKCE는 public client에 필수 (RFC 7636) — code_verifier 생성 + code_challenge=SHA256(verifier).base64url. HTTPS 없이 학습 환경 한정 — prod 진입 시 Caddy 또는 nginx + Let's Encrypt 필수.

TODO

각 항목 옆에 증거 등급. 현재 모두 planned — 실 구현 후 별도 작업에서 actually-implemented/locally-verified로 승급.

  • docker-compose.yml 작성 (keycloak + postgres + spring + nginx) — planned
  • Keycloak realm/client 설정 + JSON export — planned
  • Spring Boot Resource Server (/api/me endpoint with @AuthenticationPrincipal Jwt) — planned
  • vanilla JS SPA (login button → PKCE 생성 → callback → token storage → /api/me 호출) — planned
  • iss mismatch issue 재현 + 해결 (KC_HOSTNAME=localhost vs keycloak 시연) — planned
  • refresh_token rotation 시연 (Keycloak Revoke Refresh Token 옵션 toggle) — planned
  • HTTPS 없는 환경에서 token 노출 demonstration (Wireshark/curl로 헤더 캡처) — planned
  • (선택) Caddy reverse proxy로 HTTPS 추가 — planned

진행 중 메모

작업하며 떠오른 메모. 자유 형식.

  • AXIS_DRIFT (2026-07-18 /branch-spec 확인) — hub 가 2026-07-14 에 분류 primary 축을 배치×federation 6패턴인증 아키텍처 4패턴(AP1~AP4) 로 교정(raw/project-notes/keycloak-patterns-overview §2.3)했으나 본 노트 본문은 여전히 Phase 0 의 "P3A" 프레이밍이다. 매핑은 P3A → AP1 + cross-cutting 배포=single-EC2 (실 구현 base). hub §2.3·§8 이 "실제 rename/re-parent 은 wiki-doc-author mode=migrate 로 점진 수행(자동 mv 금지 — wikilink 영향 검토)"라고 명시하므로 본 회차에서는 본문 재작성 없이 정합 표기만 추가(제목 blockquote + 본 메모). 물리 parent_branch: 는 아직 feature-keycloak-patterns 이고, AP 그룹 소속은 hub §8 분해표가 SSOT.
  • 본 노트는 단일-EC2 실 구현 base — detail 의 owner 는 6 자식이다. hub F5 가 "실 구현 배포 = single-EC2 1벌" 로 고정하여 본 노트가 그 물리 base 이지만, 실행 단계(docker-compose / realm / SPA / iss / refresh / Spring RS)는 자식 6개가 각각 owner 다(§Cluster). 따라서 본 노트의 D2·D4·D5 는 자식 owner 결정의 요약이라 rules/consistency-contract.mdRESTATED_FOREIGN_DECISION 소지가 있다 — 사용자 작성 결정을 덮어쓰지 않고 §구현 가이드 §3 에 위임 맵을 세워 포인터를 명시한다.
  • DECISION_DRIFT 해소 (2026-07-18) — 과거 parent D4의 manual-first 문구는 historical/superseded다. 실제 코딩 순서는 owner raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce D1의 oidc-client-ts library-first이며, manual crypto.subtle PKCE는 baseline E2E 뒤의 비교 학습 단계다.
  • OWNER_SPLIT (Spring RS + aud 검증) — 2026-07-18 — 본 §Cluster 는 raw/branch-notes/feature-keycloak-spring-rs-role-mapping 을 나열하나, hub §8 dup-reconciliation 은 "Spring RS 셋업·aud 검증 = raw/branch-notes/feature-keycloak-spring-rs-audience-validator 가 owner, role-mapping 의 role→RBAC 부분만 deferred authZ" 로 정했다. §구현 가이드 §3 위임 맵은 두 owner 를 분리 지정한다(RS 셋업/aud = audience-validator, 본 노트 Cluster 의 role-mapping 은 role→RBAC deferred).
  • repo 부재 (NO_GROUND_TRUTH for impl) — 2026-07-18 확인/home/donghyeon/workspace/keycloak-patterns/ 디렉터리가 아직 없다. 따라서 모든 TODO/구현 계획 항목은 planned(코드로 확인된 actually-implemented 아님). 계약 근거는 official docs(Keycloak/Spring/OWASP/RFC)이며, 포트 값 등 배포 상수는 자식 raw/branch-notes/feature-keycloak-docker-compose-stack D6 이 owner (KC-CONTAINER-C5 가 "포트 값은 공식 raw verbatim 부재" 로 못박음 → 본 노트에서 official 로 단정 금지).
  • TOPOLOGY_DRIFT (2026-07-18 depth 감사) — 본 노트 §컴포넌트 다이어그램·§구현 가이드 §1 은 3-포트 직노출(nginx=static only, backend/Keycloak 각자 포트)로 토폴로지를 확정하나, hub raw/project-notes/keycloak-patterns-overview §3-1-5 다이어그램은 nginx proxy_pass 리버스프록시(same-origin)를 그린다. 본 노트가 토폴로지 owner(D3 · hub F5)이므로 divergence 를 공개만 하고 3-포트를 학습 기본으로 유지 — cross-origin 귀결(CORS / Web Origins)은 §구현 가이드 §1 + §엣지·실패·의존 에서 종결한다.

컴포넌트 다이어그램

EC2 (단일 호스트)
├─ nginx           (port 80)   → /index.html (vanilla JS SPA static 파일)
├─ Spring Boot     (port 8081) → /api/* (Resource Server)
└─ Keycloak        (port 8080) → /realms/<realm>/...

Browser → EC2:80   → SPA load
Browser → EC2:8080 → Keycloak (OIDC redirect: /auth → 로그인 → /callback)
Browser → EC2:8081 → Backend (Authorization: Bearer <access_token>)
Backend → EC2:8080/realms/<realm>/protocol/openid-connect/certs (JWKS, localhost network)

신뢰 경계: 단일 호스트 내 localhost trust. 외부에서는 EC2 public IP / DNS만 노출.

토큰 교환 sequence (P2A와 동일 + localhost 특이점)

  1. SPA: PKCE 생성code_verifier (랜덤 43128 char), code_challenge = BASE64URL(SHA256(code_verifier)), code_challenge_method=S256. verifier는 sessionStorage 저장 (단일 auth 라운드트립 수명 — 콜백 직후 폐기하므로 D2/OWASP 의 장기 토큰 저장 금지와는 별개다. 단 sessionStorage 자체는 XSS 노출면이라 raw/official-docs/oauth2-pkce-rfc-7636.md Usage Boundaries 가 별도 플래그).
  2. SPA → Keycloak /auth redirect — query: client_id, redirect_uri, response_type=code, scope=openid, state, code_challenge, code_challenge_method=S256.
  3. 사용자 로그인 → Keycloak → redirect_uri callback with ?code=...&state=....
  4. SPA → Keycloak /token (POST form)grant_type=authorization_code, code, redirect_uri, client_id, code_verifier. 응답: access_token / refresh_token / id_token / expires_in.
  5. SPA → Backend Authorization: Bearer <access_token>.
  6. Backend → Keycloak JWKS (localhost:8080/realms/<realm>/protocol/openid-connect/certs) → public key fetch (캐시) → JWT signature verify + iss claim 검증.

단일 호스트 특이점

KC_HOSTNAME 함정 (이 패턴의 핵심 학습 포인트)

  • iss claim은 Keycloak이 발급한 JWT 안에 박힘. 예: iss=http://localhost:8080/realms/keycloak-patterns.
  • Browser는 localhost:8080으로 Keycloak에 접근, backend도 같은 hostname을 issuer-uri로 등록해야 검증 통과.
  • Docker Compose에서 backend가 keycloak:8080(컨테이너 DNS)로 JWKS를 부르면 issuer mismatch 발생 (token에 박힌 isslocalhost:8080인데 backend가 기대하는 issuer가 keycloak:8080).
  • 해결: backend issuer-uri = http://localhost:8080/realms/... 로 통일. JWKS도 같은 hostname으로 부르려면 컨테이너에서 호스트 네트워크 공유(network_mode: host) 또는 extra_hosts: [host.docker.internal:host-gateway]host.docker.internal 사용.
  • 또는 Keycloak KC_HOSTNAME_BACKCHANNEL_DYNAMIC=true로 frontchannel/backchannel URL 분리 (Keycloak 24+).

redirect_uri mismatch

  • Keycloak client 등록 시 Valid redirect URIs 정확히 일치해야 함.
  • http://localhost/callbackhttp://127.0.0.1/callbackhttp://<ec2-public-ip>/callback. 셋 다 다른 URI.
  • 와일드카드 http://localhost/* 허용은 학습 환경 한정. prod 금지.

기타

  • PKCE는 여전히 필수 — public client (브라우저는 client_secret 보관 불가).
  • HTTPS 없으면 token 평문 노출access_token, refresh_token이 HTTP 헤더/응답으로 평문 전송. 학습 환경 한정.
  • nginx는 단순 static 파일 서빙 (Caddy 또는 nginx + Let's Encrypt로 HTTPS termination 추가 가능).

장점 / 단점

장점

  • 단일 호스트라 네트워크 디버깅 쉬움. tcpdump / docker compose logs로 한 화면에서 추적.
  • docker-compose 한 줄로 환경 reset (docker compose down -v && up).
  • 학습 곡선 평탄. k8s / ingress / Traefik 등 부가 인프라 없음.
  • localhost trust로 보안 변수 최소화 — 외부 노출은 80/8080/8081 세 포트만.

단점

  • 운영 환경 모방 X. 실 운영은 Keycloak / API / static 분리 배치(P1·P2 패턴) — 본 패턴은 학습 전용.
  • HTTPS termination 별도 처리 필요. Caddy reverse proxy를 앞단에 두거나 nginx에 cert 추가.
  • 단일 EC2 장애 = 전체 다운. SPOF.
  • KC_HOSTNAME 설정 잘못 시 디버깅 난이도 급증 (issuer/redirect/JWKS URL 3가지가 얽힘).

결정 사항 (decisions)

  • 2026-05-25: HTTPS 없이 진행 (학습 환경). prod 진입 시 Caddy 또는 nginx + Let's Encrypt 추가. 이유: cert 발급/갱신 흐름이 본 학습 주제(OIDC)와 무관.
  • 2026-05-25: pure SPA의 access/refresh token은 모두 memory-only로 둔다. reload 시 복원하지 않고 재인증한다. HttpOnly refresh cookie는 TMB/BFF variant이며 본 AP1 baseline이 아니다.
  • 2026-05-25 (owner 위임): issuer identity와 JWKS network address의 실행 wiring은 raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch D6을 따른다. 본 base는 KC_HOSTNAME 함정의 배포 축만 소유한다.
  • 2026-05-25 (historical, superseded): vanilla JS는 manual fetch + crypto.subtle 기반 PKCE 구현 우선.
  • 2026-07-18: 실제 코딩 순서는 raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce D1의 oidc-client-ts library-first다. manual PKCE는 baseline E2E 뒤 비교 학습 단계다.
  • 2026-05-25: realm 1개 + client 1개 (spa-client, public, Standard Flow + PKCE S256 강제). multi-tenant / role mapping은 out of scope.

결정-근거 매핑

각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. P3A 는 학습 환경 단순화 결정 다수 → 일부는 UNSUPPORTED_DECISION (공식 근거 없이 학습 우선순위 기반).

선택 조건 열(R2, 2026-07-18 추가): "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면 N/A.

Ownership note — 본 노트는 단일-EC2 배포 base 다. D1·D3 은 본 branch 고유(HTTPS 경계 · KC_HOSTNAME 배포 축)이고, D2·D4·D5 는 자식 owner 결정의 요약이다(§구현 가이드 §3 위임 맵 · §진행 중 메모 RESTATED_FOREIGN_DECISION). 세부는 owner 를 정본으로 본다.

Decision ID Decision 선택 조건 (언제 이 결정 / 언제 대안) Supporting Claims Evidence Strength Open Risk
D1 학습 환경에서 HTTPS 없이 진행 (Caddy / Let's Encrypt 는 prod 진입 시 추가) 학습 환경(localhost / 단일 EC2)에서 OIDC lifecycle 자체가 학습 목표일 때만 HTTP. 외부 노출·prod 진입 시 HTTPS edge termination 필수(KC-RP-C4). 또한 frontchannel 이 HTTPS 여야만 브라우저 crypto.subtle(S256 계산)이 secure context 로 동작 — localhost 예외에만 HTTP 허용(§구현 가이드 §2 UNSUPPORTED_IMPL_DECISION) UNSUPPORTED_DECISION — 공식 문서는 prod 에서 HTTPS edge termination 을 권고 (raw/official-docs/keycloak-reverseproxy-official.md#KC-RP-C4) 이고, 학습 환경에서 HTTP 만으로 OIDC 를 진행하라는 권고는 어느 공식 자료에도 없음 (학습 우선순위 기반 결정) HTTP 위에서 토큰이 평문 전송 → 학습 환경 외 노출 시 즉시 노출. KC-CONTAINER-C3 (start-dev insecure defaults) 와 결합 시 prod 절대 금지
D2 pure SPA의 access/refresh token을 모두 memory-only로 보관하고 reload 시 재인증 AP1 pure SPA baseline이면 memory-only. 세션 지속이 요구되면 HttpOnly refresh cookie를 슬쩍 추가하지 않고 AP2(TMB) 또는 AP3(BFF) variant로 전환한다. owner = raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff D1, 구현 consumer = raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce D2 raw/official-docs/owasp-html5-storage-xss-spa.md#OWASP-HTML5-C1, raw/official-docs/owasp-html5-storage-xss-spa.md#OWASP-HTML5-C2, raw/official-docs/owasp-html5-storage-xss-spa.md#OWASP-HTML5-C4 official-reference (owner를 통한 위임) memory token도 실행 중 XSS에 노출된다. reload UX를 허용할 수 없으면 별도 server-side custody·CSRF 계약을 갖는 variant가 필요
D3 KC_HOSTNAME이 정하는 issuer identity와 backend의 JWKS 도달성을 함께 맞춘다 실행 profile의 정확한 값과 network mechanism은 raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch D6이 owner다. 본 base는 단일-host에서 두 조건이 모두 필요하다는 배포 requirement만 소유 raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C2, raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C3, raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C1, raw/official-docs/spring-security-resource-server-jwt.md#SSRS-JWT-C2 official-vendor-doc + delegated 실제 Docker profile은 owner D6의 401→200 E2E 전까지 planned
D4 실제 코딩 순서 = oidc-client-ts 우선, manual PKCE는 비교 학습용 별도 단계 baseline E2E를 먼저 확보할 때 library-first. 내부 알고리즘 비교는 이후 manual 단계. 실행 owner = raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce D1 owner D1의 OIDCTS-C2/C3 + raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C2, #PKCE-RFC7636-C3 delegated official-vendor-doc + official-standard manual 단계의 crypto.subtle secure-context 동작은 별도 확인 필요
D5 realm 1개 + client 1개 (spa-client, public, Standard Flow + PKCE S256 강제) 단일 패턴 학습이면 realm 1 / client 1(spa-client public). hub F3 대로 4 패턴 통합 base 로 확장 시 realm 은 공유 1개 유지, client 는 패턴당 1개(spa-public / token-mediating-confidential / bff-confidential / edge-proxy)로 분리 — aud 로 client 구분. multi-tenant / role mapping 은 out of scope. owner = raw/branch-notes/feature-keycloak-realm-client-export D1 (public+PKCE S256) raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C1, raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C3, raw/official-docs/keycloak-getting-started-docker.md#KC-GSD-C3, raw/official-docs/keycloak-getting-started-docker.md#KC-GSD-C4 official-standard + official-vendor-doc Keycloak Admin UI 에서 PKCE S256 강제 옵션의 정확한 토글명/위치는 공식 quickstart 인용에 없음 — Admin UI 실 확인 필요

구현 가이드

본 branch 는 단일-EC2 배포 통합 base (hub F5) — 실행 코드가 아니라 (1) 물리 토폴로지 경계 명세, (2) KC_HOSTNAME/redirect_uri 배포측 signature 함정, (3) 6 자식 owner 로의 위임 맵이 산출물이다. 아래 sub-section 은 본 branch 고유 결정(D1·D3·D5)에서만 도출하며, 자식이 owner 인 실행 detail 은 재진술하지 않고 포인터로 위임한다(rules/consistency-contract.md Reference-Only). 실 구현 등급은 모두 planned(repo 부재 — §진행 중 메모 NO_GROUND_TRUTH).

1. 단일 EC2 토폴로지 경계 — 누가 어디서 무엇을 하는가

Trace: D3 (KC_HOSTNAME 통일 — KC-HOST-C2/KC-HOST-C3), D5 (realm/client — PKCE-RFC7636-C1/KC-GSD-C3), hub F5 (single-EC2 배포 base). 본 §가 §컴포넌트 다이어그램을 결정-trace 로 종결한다.

  • UNSUPPORTED_IMPL_DECISION: 포트 값(80/8080/8081)·네트워크 메커니즘·컨테이너명은 어느 official 인용도 강제하지 않는다(KC-CONTAINER-C5 가 "포트/env 값은 공식 raw verbatim 부재" 로 명시). owner 는 자식 raw/branch-notes/feature-keycloak-docker-compose-stack D6 — 본 §는 경계와 노출 정책만 확정하고 상수는 위임한다.
  • TOPOLOGY / CORS 귀결 (2026-07-18 depth 감사 반영): 본 §가 확정한 3-포트 직노출(nginx=static only)은 브라우저에 2개의 cross-origin 레그를 만든다 — SPA(:80)→Keycloak(:8080) /token + SPA(:80)→backend(:8081) /api+Authorization. 따라서 Keycloak Web Origins(KC-GSD-C4 — "Set Web origins to ...") + backend Spring CORS 가 필수다(§엣지·실패·의존 CORS 경로 + §3 위임 맵). ⚠️ TOPOLOGY_DRIFT: hub §3-1-5 다이어그램은 nginx proxy_pass 리버스프록시(same-origin)를 그리나 본 노트는 3-포트 직노출을 그린다(§진행 중 메모 TOPOLOGY_DRIFT). 본 노트가 토폴로지 owner(D3 · hub F5)이므로 학습-최소 3-포트를 기본으로 두되, proxy_pass 채택 시 /api 는 same-origin 화되어 Spring CORS 가 소거된다 — 그래도 SPA→Keycloak /token 은 여전히 cross-origin 이라 Web Origins 는 토폴로지와 무관하게 필수.
컴포넌트 역할 (무엇을 보유 / 수행) 외부 노출 근거 등급
nginx vanilla JS SPA static 서빙 (public client) :80 (외부) §컴포넌트 다이어그램; 포트 상수는 child raw/branch-notes/feature-keycloak-docker-compose-stack D6 planned
Spring Boot (Resource Server) 토큰 미보유 — 요청마다 JWT를 검증하고 audience 계약을 적용 :8081 (외부) raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6 planned
Keycloak (Authorization Server) 토큰 발급 + JWKS publish, KC_HOSTNAME 로 issuer hostname 고정 :8080 (외부) KC-CONTAINER-C1 (KC_HOSTNAME=노출 주소), KC-HOST-C2; realm 모델 KC-GSD-C3 planned
PostgreSQL Keycloak realm/user persistence 내부 only (미노출) child raw/branch-notes/feature-keycloak-docker-compose-stack D2 (dev-file 대신 postgres) planned
trust 경계 단일 host localhost trust — 외부는 위 3 포트만, backend↔Keycloak JWKS 는 loopback D3 (localhost 통일); §컴포넌트 다이어그램 신뢰 경계 planned

2. 배포측 signature 함정 — KC_HOSTNAME(iss) + redirect_uri

Trace: D3 (KC-HOST-C2 hostname 의무·dynamic resolution 차단, KC-HOST-C3 fraudulent issuer 방어), D5 (KC-GSD-C4 Valid redirect URIs 설정). 본 §는 단일 host 배포에서만 발생하는 고유 관심사이며 §단일 호스트 특이점을 결정-trace 로 종결한다.

  • issuer/JWKS의 실행 profile은 child raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch D6이 owner다. 본 §는 함정의 존재·재현 조건·해결 축만 확정하고 구체 mechanism을 복제하지 않는다.
  • UNSUPPORTED_IMPL_DECISION: 브라우저 crypto.subtle(S256 계산)은 secure context 에서만 노출되어 non-localhost HTTP origin 에선 차단된다. trade-off: 학습 환경의 localhost 예외에 의존해 HTTP 를 쓰되(D1), 그 외에는 frontchannel = HTTPS 로 둔다 — 본 corpus 에 이 브라우저 제약의 직접 인용 없음(oauth2-pkce-rfc-7636.md Usage Boundaries 도 "추가 확인 필요"로만 기록, §Claims To Verify).
함정 발생 (재현 조건) 기대 동작 / 해결 근거 owner (실행)
iss mismatch browser 는 토큰의 iss=http://localhost:8080/realms/... 를 받고, backend 가 keycloak:8080(컨테이너 DNS)을 기대 issuer 로 설정 → 모든 요청 401 KC_HOSTNAME=localhost 통일 + backend issuer-uri 동일 값 + loopback 도달 메커니즘 KC-HOST-C2, KC-HOST-C3, SSRS-JWT-C1 재현·해결 → raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch D1/D4/D6
redirect_uri mismatch http://localhost/callbackhttp://127.0.0.1/callbackhttp://<public-ip>/callback — scheme·host·port·trailing slash 한 글자만 달라도 authorization request 거부 등록 URI 와 접근 hostname 1:1 일치 또는 둘 다 등록. wildcard /* 는 학습 한정 KC-GSD-C4 (Valid redirect URIs 설정 — vendor). exact-match MUST 표준(OA21-C5)은 본 노트 Sources 밖 → raw/branch-notes/feature-keycloak-internal-spa-direct-no-google D7 이 owner-absent 로 흡수(Reference-Only) wildcard 정책 → raw/branch-notes/feature-keycloak-realm-client-export D2; 실 redirect_uri 값 → raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce D5 (callback.html)
crypto.subtle secure context non-localhost HTTP origin 에서 crypto.subtle.digest('SHA-256', ...) 차단 → S256 challenge 계산 불가 → PKCE 흐름 실패 localhost 학습만 HTTP 허용, 그 외 frontchannel = HTTPS UNSUPPORTED_IMPL_DECISION (본 corpus 직접 인용 없음, §Claims To Verify) raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce

3. 결정 위임 맵 (6 자식 owner — Reference-Only)

Trace: D2·D4·D5 는 본 base 가 요약만 보유하고 실행 detail 의 owner 는 자식이다(§진행 중 메모 RESTATED_FOREIGN_DECISION). rules/consistency-contract.md Single-Owner 에 따라 세부는 owner 를 정본으로 본다 — 본 표는 포인터 + 1줄 요약만 유지하고 임계값·메커니즘을 재진술하지 않는다.

  • UNSUPPORTED_IMPL_DECISION: 없음 — 각 행은 owner 노트의 실존 D<n> 을 가리킨다(2026-07-18 확인).
관심사 owner (정본) owner 결정 1줄 요약 (본 base 의 인용)
docker-compose 스택 (keycloak+postgres+nginx+spring, healthcheck, realm auto-import) raw/branch-notes/feature-keycloak-docker-compose-stack D1~D6 start-dev + postgres + depends_on: service_healthy + --import-realm + KC_HOSTNAME=localhost + 포트/.env secret §컴포넌트 다이어그램·§구현 계획의 docker-compose 항목 정본
realm/client 설정 + JSON export raw/branch-notes/feature-keycloak-realm-client-export D1~D5 public + PKCE S256, redirect wildcard(학습), rotation 값, AT 5분, export+redact commit 본 base D5 요약의 정본
vanilla JS SPA PKCE 코드 raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce D1~D5 library-first baseline, manual은 비교 학습 단계 본 base D4와 정렬 완료
iss 함정 재현·해결 raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch D1/D4/D6 issuer identity/JWKS reachability 실행 profile 본 base D3·§구현 가이드 §2 의 재현·해결 위임
refresh rotation + logout raw/branch-notes/feature-keycloak-refresh-rotation-and-logout D1/D2/D4/D5 rotation ON + Max Reuse 0, AT 5분, revoke/logout 분리, rotation flow 시연 본 base D2(refresh 저장) 인접 — rotation 정책 정본
Spring RS + aud validator raw/branch-notes/feature-keycloak-spring-rs-audience-validator D1/D6 RS 공통 셋업과 단일 backend audience 검증 본 base 백엔드 authN 검증 owner
role→RBAC (deferred) raw/branch-notes/feature-keycloak-spring-rs-role-mapping D4~D6 realm/client role을 Spring authority로 변환·강제 4패턴 authN E2E 이후 착수
CORS 경계 (Keycloak Web Origins + Spring CORS) Web Origins → raw/branch-notes/feature-keycloak-realm-client-export · Spring CORS → SecurityFilterChain (RS 셋업 owner = raw/branch-notes/feature-keycloak-spring-rs-audience-validator per OWNER_SPLIT) Web Origins 에 SPA origin 등록(KC-GSD-C4) + Spring CORS 로 SPA origin whitelist 3-포트 직노출의 cross-origin 귀결(§구현 가이드 §1 · §엣지·실패·의존) — 본 base 는 경계·owner 만 지정, 값은 owner

엣지·실패·의존

R4(깊이 게이트) 캡처용. 본 base 는 planned(repo 부재) 이나, 단일-EC2 스택을 실제로 세울 때 부딪힐 실패/엣지와 다른 계약 의존을 미리 열거한다.

검증해야 할 주장

공식 문서는 근거지만, P3A 학습 환경에서의 실제 동작은 별도 검증 필요.

Claim Why uncertain How to verify Status
KC_HOSTNAME=localhost 가 docker-compose 컨테이너 내부에서 의도대로 작동 (token iss=http://localhost:8080/realms/...) 공식 hostname guide 는 localhost 사용 권고가 dev/quickstart 한정. 학습 환경에서 host 네트워크 의존성이 컨테이너 격리와 충돌 가능 docker compose up -d 후 access token 발급 → jwt.io 또는 jqiss claim 확인 needs-confirmation
network_mode: host 가 Linux EC2 에서 정상 동작 (Docker Desktop 가정 제약 회피) Linux 호스트는 host 네트워크 지원, mac/Windows Docker Desktop 은 제약. 학습 환경이 EC2 Linux 인지 로컬 Docker Desktop 인지에 따라 결과 다름 EC2 ubuntu 에서 docker compose ps + curl http://localhost:8080/realms/keycloak-patterns/.well-known/openid-configuration 확인 planned
backend (spring-boot-starter-oauth2-resource-server) 가 issuer-uri=http://localhost:8080/... 로 startup 시 JWKS discovery 성공 SSRS-JWT-C2 는 4단계 discovery 를 보장하지만 컨테이너 → 호스트 loopback 도달성은 별도 backend 로그에서 JwtDecoder 초기화 메시지 + /api/me 호출 결과 확인 planned
crypto.subtle.digest('SHA-256', ...) 가 학습 환경 (http://localhost) Secure Context 예외로 사용 가능 일반적 HTTP origin 은 Secure Context 아님 → SubtleCrypto 차단. localhost 는 브라우저 vendor 별 예외 처리 Chrome/Firefox 에서 app.js 콘솔에 await crypto.subtle.digest(...) 호출 확인 needs-confirmation
redirect_uri=http://localhost/callback 등록 후 http://127.0.0.1/callback 으로 callback 시 Keycloak 이 거부 (의도된 mismatch 시연) OA21-C5 exact-match 표준은 있으나 Keycloak 의 실제 enforce 동작 (대소문자, trailing slash, host 동등성) 은 별도 Admin UI 에서 valid redirect URIs 등록 후 hostname 변형 시 redirect_uri_mismatch 에러 확인 planned
Keycloak Revoke Refresh Token: ON + Max Reuse: 0 토글이 refresh rotation 을 실제로 한 번만 허용 공식 인용 부재 (oauth2.1 OA21-C3 는 scope/resource binding 만 언급) refresh token 두 번 연속 사용 → 두 번째 호출에서 4xx 응답 확인 planned

마주친 문제

  • (구현 시작 후 추가) KC_HOSTNAME 설정 misconfiguration으로 인한 issuer mismatch 예상.
  • (구현 시작 후 추가) redirect_uri 등록 시 localhost vs 127.0.0.1 혼동 예상.

구현 계획

  • Repo 위치: /home/donghyeon/workspace/keycloak-patterns/ (별도 git repo, LLM Wiki 외부). ⚠️ 2026-07-18 현재 미존재 — §진행 중 메모 NO_GROUND_TRUTH. 아래 전부 planned.
  • docker-compose.yml:
    • keycloak (quay.io/keycloak/keycloak:26.x, start-dev, KC_HOSTNAME=localhost, KC_HTTP_ENABLED=true, KC_BOOTSTRAP_ADMIN_USERNAME=admin)
    • postgres (Keycloak realm persistence, volume mount)
    • backend (Spring Boot 3 + Java 21, spring-boot-starter-oauth2-resource-server)
    • nginx (static SPA serve, port 80)
    • (선택) caddy reverse proxy for HTTPS
    • (실행 detail 정본: raw/branch-notes/feature-keycloak-docker-compose-stack D1~D6)
  • SPA: index.html + app.jsraw/branch-notes/feature-keycloak-vanilla-js-spa-pkce D1에 따라 oidc-client-ts로 baseline E2E를 먼저 만들고 manual PKCE는 비교 단계에서 수행한다.
  • Backend:
  • Keycloak realm export JSON: keycloak-patterns-realm.json (realm + client + 테스트 사용자) commit. (실행 detail 정본: raw/branch-notes/feature-keycloak-realm-client-export D1~D5)

관련

묶음 (자식 sub-sub-branches — P3A 실 구현 6단계)

Sources는 상단 "외부 근거" 섹션. Errors/Interview/Lectures/Blog 는 현재 없음 — P3A 실 구현(Phase 3) 진입 시 errors / interview-prep 등재 예상.

관련 일일 노트

완료 후 정리

  • PR 링크: (별도 keycloak-patterns repo)
  • 리뷰 메모:
  • 머지 결과 / 배포 환경: 단일 EC2(또는 로컬 docker-compose) 시뮬레이션, 로컬 검증까지.
  • wiki 추출 대상 (verified만, wiki/projects/로만 추출):
    • actually-implemented 항목: (구현 후 채움)
    • locally-verified 항목: (구현 후 채움)
    • prod-verified 항목: (없음, prod 배포 out of scope)
  • 추출하지 않을 항목 (planned / documented-only / abandoned): 현재 전부 planned. 구현 완료된 부분만 wiki/projects/로 승급.