Files
llm-wiki/raw/branch-notes/feature-keycloak-realm-client-export.md
T

282 lines
27 KiB
Markdown

---
title: branch / feature-keycloak-realm-client-export (Keycloak realm/client 설정 + realm JSON export)
source_type: branch-note
status: raw
id: BR-KEYCLOAK-PATTERNS-OVERVIEW-002
kind: project-work-item
project: keycloak-patterns-overview
work_item: WI-KEYCLOAK-PATTERNS-OVERVIEW-002
inherits: [DEC-KEYCLOAK-PATTERNS-OVERVIEW-REALM-CLIENT-001@1, DEC-KEYCLOAK-PATTERNS-OVERVIEW-SECRET-BOUNDARY-001@1]
refines: []
overrides: []
depends_on: [WI-KEYCLOAK-PATTERNS-OVERVIEW-001]
contract_packet: 1
branch: feature-keycloak-realm-client-export
parent_branch:
related_projects: [keycloak-patterns]
tags: [branch, keycloak-patterns, p3a, implementation, keycloak-realm, pkce, oidc-client]
created: 2026-05-25
target_merge:
status_label: in-progress
contract_packet_sha256: f751af30be9944511f5759f72096e406a9d6e191e075904df611b11f5797c5df
---
# branch: feature-keycloak-realm-client-export (Keycloak realm/client 설정 + JSON export)
> Layer: `raw/branch-notes/` — [[raw/project-notes/keycloak-patterns-overview]] Work Item의 직접 자식 branch.
> **P3A는 실 구현 대상**. 본 sub-sub는 학습 + 작업 plan 기록 — 실 구현은 `/home/donghyeon/workspace/keycloak-patterns/`.
<!-- section-id: branch-parent -->
## 부모 (필수)
[[raw/project-notes/keycloak-patterns-overview]]
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: realm import와 client 4개 등록 후 unauthenticated protected endpoint가 401이고 export가 재현된다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-KEYCLOAK-PATTERNS-OVERVIEW-REALM-CLIENT-001@1` | 단일 realm keycloak-patterns에서 인증 패턴별 client를 분리한다 | realm import와 인증 패턴별 client export 구성에 적용한다 | [[raw/project-notes/keycloak-patterns-overview]] |
| `DEC-KEYCLOAK-PATTERNS-OVERVIEW-SECRET-BOUNDARY-001@1` | confidential client secret은 env var로 주입하고 commit·realm export 평문을 금지한다 | realm export artifact의 secret redaction과 주입 경계에 적용한다 | [[raw/project-notes/keycloak-patterns-overview]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
> 기존 branch-local 결정은 아래 `## Decision Evidence Map`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
없음.
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
`keycloak-patterns` realm + `spa-client` (public, PKCE S256 강제) + 테스트 user 2명 + role 2개를 설정하고 realm JSON export를 commit한다. import 배선은 [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D4가 소유하며, 본 문서는 그 consumer가 사용할 JSON artifact의 내용·검증 계약만 소유한다.
면접 질문: "Keycloak에서 public client에 PKCE 강제는 어떻게 거나요?"
→ "대상 Keycloak Client의 Capability config에서 `PKCE method = S256`을 지정합니다. 실제 export의 client attribute와 verifier 없는 요청의 거부 응답은 배포 버전에서 확인합니다. RFC 7636 관점에서 client_secret을 안전하게 보관할 수 없는 public SPA의 code interception 위험을 PKCE로 완화합니다."
- 이슈:
- PR: (별도 keycloak-patterns repo)
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- realm `keycloak-patterns` 생성
- Client `spa-client` (public, Standard Flow + PKCE S256 강제)
- Valid Redirect URIs (`http://localhost/*`, `http://127.0.0.1/*`)
- Web Origins (`+` — Valid Redirect URI에서 자동 도출)
- User 2명 (`admin-user` / `regular-user`) + 초기 password
- Role 2개 (`admin-role` / `user-role`) + user에 매핑
- Refresh Token Rotation 설정 필드와 target-version 실험 후보값 기록(현재 후보: ON / `Max Reuse: 0`; 의미는 owner 실험 전 확정하지 않음)
- Realm JSON export 파일 commit (`./realm-export.json`)
- import consumer가 사용할 realm JSON artifact의 파일명·내용·redaction 검증 계약
### 제외 범위
- Google IdP 추가 (P3B → [[raw/branch-notes/feature-keycloak-single-ec2-google-federation]])
- 세밀한 role hierarchy / composite role
- group / organization
- 본격적인 password policy / OTP
- `--import-realm`, volume mount, container command 등 import 배선 — [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D4 소유
## 근거 (필수, 최소 1개+)
> 노트 최초 작성(2026-05-25) 당시 근거 raw 가 부재해 D1/D2/D5 가 `UNSUPPORTED_DECISION` 이었으나, 이후 corpus 성장으로 아래 raw 들이 추가되어 official 근거로 승격했다(재조사 없이 기존 raw 재매핑). 상세는 §Audit & Findings `EVIDENCE_UPGRADE`.
| Source | 정당화하는 결정 |
|---|---|
| [[raw/official-docs/oauth2-pkce-rfc-7636]] | D1 — public client 의 code interception 취약성 + S256 공식 + token endpoint 의 verifier 불일치 거부 (`PKCE-RFC7636-C1/C3/C4`) |
| [[raw/official-docs/keycloak-client-pkce-method-enforcement-official]] | D1 — Keycloak client 단위 PKCE 강제 옵션의 정식 명칭("PKCE method")과 S256 값 동작 (`KC-PKCE-C1/C3`) |
| [[raw/official-docs/oauth2-browser-based-apps-ietf-draft]] | D1 — 브라우저 앱 = public client(client credentials 없음) 표준 정의 (`OAUTH-BBA-C3`) |
| [[raw/official-docs/oauth-v2-1-draft-ietf]] | D1 — 모든 client PKCE MUST + AS enforce MUST (`OA21-C1`); D2 — redirect URI exact-match MUST (`OA21-C5`); D3 — refresh token scope/RS bound + code flow 발급 경로 (`OA21-C3/C6`) |
| [[raw/official-docs/keycloak-getting-started-docker]] | D2 — quickstart 자체가 redirect URI 를 `.../*` wildcard + Web origins 정확값으로 설정 (`KC-GSD-C4`); realm=tenant, client 등록 절차 (`KC-GSD-C3`) |
| [[raw/official-docs/keycloak-import-export-realms]] | D5 — `--import-realm` startup import + 컨테이너 import dir(`/opt/keycloak/data/import`) + 기존 realm skip(멱등) + offline `--override` 차이 (`KC-IMPORT-C1..C4`) |
| [[raw/official-docs/keycloak-server-containers-docker]] | D5 — 컨테이너 실행/env context; 정확한 env 이름·기본 포트는 `KC-CONTAINER-C5``needs-confirmation` |
## TODO
- [ ] Keycloak admin console 접속 (`http://localhost:8080`, admin 계정) — 등급: `planned`
- [ ] realm `keycloak-patterns` 생성 — 등급: `planned`
- [ ] Client `spa-client` 생성: Access Type `public`, Standard Flow Enabled, Direct Access Grants Disabled — 등급: `planned`
- [ ] Client Capability config: `PKCE method = S256`; 생성된 realm export의 내부 key도 함께 확인 — 등급: `planned`
- [ ] Client Valid Redirect URIs: `http://localhost/*`, `http://127.0.0.1/*` 양쪽 등록 — 등급: `planned`
- [ ] Client Web Origins: `+` (Redirect URIs에서 자동 도출) — 등급: `planned`
- [ ] Role 생성: realm role `admin-role`, `user-role` — 등급: `planned`
- [ ] User 생성: `admin-user` (password 초기화, `admin-role` 부여) — 등급: `planned`
- [ ] User 생성: `regular-user` (password 초기화, `user-role` 부여) — 등급: `planned`
- [ ] Realm Settings → Tokens: owner 실험 profile에 따라 `Revoke Refresh Token``Refresh Token Max Reuse` 값을 설정하고 export에 기록(0/1의 의미는 사전 단정 금지) — 등급: `planned`
- [ ] Realm Settings → Tokens: Access Token Lifespan 5분 (학습용 짧게) — 등급: `planned`
- [ ] Realm export: admin console → Export 또는 `kc.sh export --realm keycloak-patterns --file /opt/keycloak/data/import/realm-export.json` — 등급: `planned`
- [ ] export JSON에서 secret/password 제거(또는 placeholder 치환) 후 git commit — 등급: `planned`
- [ ] [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D4가 본 artifact를 소비하는지 acceptance 검증(경로·파일명·realm/client 존재 확인); mount/command 저작은 하지 않음 — 등급: `planned`
- [ ] 환경 reset 후 consumer import acceptance 검증 (`docker compose down -v && up` → admin 로그인 → realm/client 존재 확인) — 등급: `planned`
## 진행 중 메모
- **PKCE는 Keycloak public client 기본 권장.** S256만 허용(`plain` 거부)이 보안 표준.
- `Valid Redirect URIs``localhost``127.0.0.1` 둘 다 등록하는 이유: 브라우저가 어느 호스트로 SPA를 로드하느냐에 따라 redirect URI도 달라짐. 두 URI는 Keycloak이 다른 것으로 본다.
- Web Origins `+`는 Redirect URIs 도메인을 자동으로 CORS allow에 추가. wildcard `*`는 학습에서도 비권장.
- Realm export의 credential 포함 여부·표현 형식은 Keycloak 버전과 export mode에 따라 달라질 수 있으며 현재 근거로 확정할 수 없다. target image의 `kc.sh export --help`, 실제 JSON, re-import 후 로그인까지 확인하기 전에는 password 포함/미포함을 모두 가정하지 않는다. 기본 절차는 credential을 별도 bootstrap/reset하고 commit 전 민감 필드를 redact하는 것이다.
- `--import-realm`은 Keycloak 19+ 부터 지원 (자동 import). 구버전은 `kc.sh import` 별도 실행.
## 결정 사항 (decisions)
- 2026-05-25: **public client + PKCE S256 강제.** 이유: vanilla JS SPA는 client_secret 보관 불가 (RFC 7636), public client + PKCE가 표준.
- 2026-05-25: **Redirect URI에 wildcard `/*` 사용.** 이유: 학습 환경 한정 (localhost callback 경로 자유로움). prod에서는 정확한 경로 하나만.
- 2026-05-25 (Historical / superseded rationale): **Refresh Token Rotation ON + Max Reuse 0**을 곧바로 보안 정책으로 확정했으나, `Max Reuse` 의미와 reuse 후 family 동작은 target-version 실험 전 단정할 수 없다. 현 결정은 [[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D1/D2의 정책·검증 계약을 소비해 확정된 값을 export에 기록하는 것이다.
- 2026-05-25: **Access Token Lifespan 5분.** 이유: rotation/revoke 시연 시 access token이 즉시 invalidate 안 됨을 짧게 검증.
- 2026-05-25: **Realm JSON export commit.** 이유: 환경 reset 1줄 정책 (`docker compose down -v && up` 후 실 import).
## 결정-근거 매핑
> 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. `Decision ID` 는 본 branch-note 안에서 안정적으로 유지.
> `선택 조건` 열: "이 조건일 때 이 결정, 다른 조건이면 어떤 대안". 분기 없으면 `N/A`.
> D3/D4/D5 는 sibling owner 브랜치에 rationale/wiring 을 **위임(delegate)** 한다 — Single-Owner(consistency-contract) 준수, 재진술(RESTATED_FOREIGN_DECISION) 금지. 상세는 §Audit & Findings.
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | public client + PKCE `S256` 강제 (vanilla JS SPA 는 client_secret 보관 불가) | SPA 가 client_secret 을 안전 보관 못할 때 이 결정 (`OAUTH-BBA-C3`, `PKCE-RFC7636-C1`). backend 를 둘 수 있으면 → 대안 BFF confidential client (`OA21-C4`) | `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/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C4`, `raw/official-docs/keycloak-client-pkce-method-enforcement-official.md#KC-PKCE-C1`, `raw/official-docs/keycloak-client-pkce-method-enforcement-official.md#KC-PKCE-C3`, `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C1` | `official-standard + official-vendor-doc` | `code_challenge_method=plain`/verifier 없는 요청의 **4xx 거부**는 `KC-PKCE-C3` 가 증명 안 함(강제 적용 암시만) → §Claims To Verify 로 실측. Admin UI 라벨은 "PKCE method"(§Audit `NAMING_DRIFT`) |
| D2 | Redirect URI 에 wildcard `/*` 사용 (localhost 학습 한정) | localhost/학습이면 `/*` (callback 경로 자유·quickstart 도 `/*` 사용 `KC-GSD-C4`). prod 진입 시 → 대안 exact-match 단일 경로 (표준 `OA21-C5`: AS MUST reject non-exact redirect URI) | `raw/official-docs/keycloak-getting-started-docker.md#KC-GSD-C4`, `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C5` | `official-vendor-doc + official-standard` | prod 에서 `/*``OA21-C5` exact-match MUST 위반 — out of scope(Out of scope 는 아니나 prod 미대상). `KC-GSD-C4``/*` 예시만 보증, wildcard 의 보안 영향은 미증명 |
| D3 | realm export에 rotation 실험 후보값을 기록하되 의미를 재정의하지 않음 | 값·정책의 owner인 [[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D1/D2가 확정한 target-version profile을 소비한다. runtime 동작은 [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] D1/D5에서 관찰한다. | `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3`, `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C6` + delegated | `official-standard (baseline) + delegated` | `Max Reuse=0`의 의미와 refresh-token family invalidation은 배포 버전 실험 전 확정하지 않는다. 본 branch는 export에 최종 선택값만 반영한다. |
| D4 | Access Token Lifespan 실험값을 realm export에 기록 | TTL 정책은 [[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D2를 소비하고, 5분이라는 실행 편의값과 관찰은 [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] D2에서 검증한다. prod TTL은 별도 결정이다. | `UNSUPPORTED_DECISION` + delegated | `UNSUPPORTED_DECISION + delegated` | target-version export의 내부 key와 실제 만료 시간이 일치하는지 runtime acceptance 필요 |
| D5 | realm-export.json **저작 + commit** (realm/client/role/user/token 설정을 담고 민감 필드를 검토·redact) | 환경 reset 반복 + realm 즉시 복원이 목표면 export artifact를 commit. 1회성 수동 설정이면 Admin UI 수동 생성. 기존 realm 강제 덮어쓰기는 offline `import --override`(`KC-IMPORT-C4`) 검토 | `raw/official-docs/keycloak-import-export-realms.md#KC-IMPORT-C2`, `raw/official-docs/keycloak-import-export-realms.md#KC-IMPORT-C3`, `raw/official-docs/keycloak-import-export-realms.md#KC-IMPORT-C4` + import 배선 delegate [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D4 | `official-vendor-doc + delegated (wiring)` | secret/password/credential 포함 여부와 형식은 모두 `needs-confirmation`. import wiring은 compose sibling D4 owner이며 본 문서는 artifact만 소유한다. |
## 구현 가이드
> 본 branch 는 실 구현 repo(`/home/donghyeon/workspace/keycloak-patterns/`)가 **아직 부재**(§Audit `NO_GROUND_TRUTH`) → 모든 항목 `planned`. code grep 으로 `actually-implemented` 확정 불가.
> 3-rule(CLAUDE.md §15.5): R1 Reference 필수 · R2 UNSUPPORTED_IMPL_DECISION 명시 · R3 OUT_OF_BRANCH_SCOPE 정제.
### 1. realm-export.json 저작 명세 (client
> **Trace**: D1(`PKCE-RFC7636-C1/C3/C4`, `KC-PKCE-C1/C3`, `OA21-C1`) · D2(`KC-GSD-C4`, `OA21-C5`) · D3([[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D1/D2 + runtime [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] D1/D5) · D4([[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D2 + runtime [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] D2)
>
> - **UNSUPPORTED_IMPL_DECISION**:
> - client 내부 JSON 속성 key `pkce.code.challenge.method` — `KC-PKCE-C1` "Does not prove" 가 이 내부 속성명이 page 에 명시 안 됨을 명시. Admin UI 라벨("PKCE method")만 증명됨. trade-off: **export-first** 접근(Admin UI 에서 설정 → `kc.sh export` 로 정확한 key 자동 생성) 채택, hand-author JSON key 는 금지. `needs-confirmation`.
> - realm token 설정 JSON key(`revokeRefreshToken`/`refreshTokenMaxReuse`/`accessTokenLifespan`) — cited raw 에 verbatim 부재. trade-off: 마찬가지로 export-first 로 확정. `needs-confirmation`.
> - user credential의 export 포함 여부와 JSON 표현(`users[].credentials[]` 등) — cited raw 미증명. target `kc.sh export --help`, 실제 export JSON, re-import login을 확인하기 전에는 포함/미포함을 가정하지 않는다. trade-off: 별도 bootstrap/reset을 default로 두고 hand-authored credential block은 피한다. `needs-confirmation`.
| JSON path (planned, export-first 로 확정) | 값 | 근거 | 상태 |
|---|---|---|---|
| `realm` | `keycloak-patterns` | D5 / 범위 | `planned` |
| `clients[].clientId` | `spa-client` | 범위 | `planned` |
| `clients[].publicClient` | `true` | D1 `PKCE-RFC7636-C1` (SPA=public), `OAUTH-BBA-C3` | `planned` |
| `clients[].standardFlowEnabled` | `true` | 범위 (Standard Flow = Authorization Code) | `planned` |
| `clients[].directAccessGrantsEnabled` | `false` | 범위 (ROPC 비활성) | `planned` |
| `clients[].attributes."pkce.code.challenge.method"` | `S256` | D1 `KC-PKCE-C3`(UI "PKCE method"=S256). **내부 key = UNSUPPORTED_IMPL** | `needs-confirmation` |
| `clients[].redirectUris` | `["http://localhost/*","http://127.0.0.1/*"]` | D2 `KC-GSD-C4` (redirect URI 형식) | `planned` |
| `clients[].webOrigins` | `["+"]` | 범위 (Redirect URI 에서 CORS 자동 도출) | `planned` |
| `roles.realm[].name` | `admin-role`, `user-role` | 범위 | `planned` |
| `users[].username` (+ `realmRoles`) | `admin-user`(admin-role), `regular-user`(user-role) | 범위 | `planned` |
| `users[].credentials[]` (초기 password) | 포함 여부·형식 미확정. 별도 bootstrap/reset을 default로 두고 hand-author 금지 | 범위("초기 password") + §엣지 + Claims To Verify #4 | `needs-confirmation` |
| realm token: `revokeRefreshToken` | owner가 확정한 target-version 실험값(현재 candidate `true`) | D3 → [[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D1/D2. **key = UNSUPPORTED_IMPL** | `needs-confirmation` |
| realm token: `refreshTokenMaxReuse` | owner가 0/1 실험 후 확정한 값(현재 candidate `0`) | D3 → concept owner + runtime observation. **key = UNSUPPORTED_IMPL** | `needs-confirmation` |
| realm token: `accessTokenLifespan` | `300` (5분, 실행 편의 candidate) | D4 → concept owner + runtime [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] D2. **key = UNSUPPORTED_IMPL** | `needs-confirmation` |
### 2. export 절차 + secret/password redaction
> **Trace**: D5(`KC-IMPORT-C4` offline export 경로, `KC-CONTAINER` context)
>
> - **UNSUPPORTED_IMPL_DECISION**: `kc.sh export` 의 정확한 flag(`--dir` vs `--file`, `--users` 옵션)와 client secret 이 export 에 평문 포함되는지 = cited raw 미증명 → §Claims To Verify. trade-off: 실 export 1회 수행 후 JSON 을 grep 으로 확인하는 절차로 대체.
1. Admin console 에서 realm/client/role/user/token 설정 (§1 표대로).
2. export: `kc.sh export --realm keycloak-patterns --file /opt/keycloak/data/import/realm-export.json` (또는 `--dir`). `--users` 처리 정책은 §엣지 참조.
3. commit 전 redact: `grep -n 'secret\|password\|credential' realm-export.json` → 평문 노출 필드는 placeholder 치환 또는 `.gitignore`.
4. `./realm-export.json` 로 repo 에 commit.
### 3. import 배선 (OUT_OF_BRANCH_SCOPE — delegate)
> **Trace / R3**: volume mount + Keycloak 부트 command `--import-realm`는 sibling [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D4가 owner (`KC-IMPORT-C1/C2/C3/C4` 인용). 본 branch는 **realm-export.json 저작과 consumer acceptance만** 담당하며 배선을 재진술하지 않는다. 배선 계약이 바뀌면 본 export 파일 배치에 영향(§엣지·의존).
## 엣지·실패·의존
> R4 캡처: 정상 경로 외 실패/엣지 + 다른 계약 의존(대상 브랜치 + Decision ID).
- **실패·엣지 경로**:
- **export JSON 에 client secret 평문 포함 가능** — public client 는 secret 없지만 confidential 전환 시 위험. 기대 동작: commit 전 `secret` grep + redact(§구현 가이드 2-3). (§Claims To Verify #3)
- **credential 포함 여부 미확정** — export mode·version에 따라 password/credential 포함 여부와 형식이 다를 수 있다. 기대 동작: 별도 bootstrap/reset을 기본으로 하고 commit 전 `secret|password|credential` 검색, 실제 re-import login으로 검증. (§Claims To Verify #4)
- **기존 realm 존재 시 auto-import skip** — `KC-IMPORT-C3`(멱등). `docker compose down -v` 로 postgres volume 을 삭제해야 재import 됨(볼륨 잔존 시 옛 realm 유지, 새 export 반영 안 됨).
- **import dir 파일명/확장자 오류** — `KC-IMPORT-C2`: `.json` regular file 만 읽고 sub-dir 무시. 경로/확장자 오타 시 silent skip → 부트는 성공하나 realm 없음.
- **다른 계약 의존**:
- [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D4 — `--import-realm` + volume mount **배선 owner**. 그 D4의 import dir 경로/flag가 바뀌면 본 export 파일 배치 위치·이름에 영향.
- [[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D1/D2 — rotation 값·정책 owner. [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] D1/D5 — target-version 실행·관찰 owner. 결과가 바뀌면 본 realm-export.json token 섹션 동기화 필요.
- [[raw/branch-notes/feature-keycloak-single-ec2-no-google]] D3 — `KC_HOSTNAME=localhost` + issuer-uri + network 계약 owner. 이 hostname 계약이 D2의 redirect URI 값(`http://localhost/*`, `http://127.0.0.1/*`)의 전제이며, parent D3가 hostname/port를 바꾸면 함께 동기화한다. 본 client scope는 parent D5의 실 구현에 소비된다.
## 검증해야 할 주장
> 공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| 대상 Keycloak Client Capability config의 `PKCE method = S256` 설정이 token endpoint에서 `code_verifier` 없는 요청을 거부하는지 | `KC-PKCE-C1/C3`은 UI 라벨과 S256 선택을 다루지만 exact HTTP status와 version별 내부 key는 증명하지 않음 | target UI 설정 후 realm export의 내부 key를 확인하고 PKCE 없는 token request의 응답을 기록 | `needs-confirmation` |
| `--import-realm` 옵션의 정확한 명령 형식 (`docker run ... start-dev --import-realm` 또는 `kc.sh start --import-realm`) | `KC-CONTAINER-C5` 가 명시적으로 `needs-confirmation` — env/option verbatim 부재 | Keycloak all-config / import 공식 페이지 발췌 후 `raw/official-docs/` 에 추가하여 verbatim 인용 확보 | `needs-confirmation` |
| Realm export JSON 에 client secret 이 평문으로 포함될 수 있음 → gitignore / redact 필요 | 본 branch 진행 중 메모 — 1차 raw 미수집 | 실 export 수행 후 JSON 파싱 → `secret` 필드 검색 + 평문 노출 여부 확인 | `needs-confirmation` |
| target Keycloak의 export mode가 user credential을 어떤 조건·형식으로 포함하는지 | `usersExport=true`와 password 포함을 연결하는 1차 근거가 없고 버전별 CLI 옵션 차이 가능 | target image에서 `kc.sh export --help` 확인 → 실제 JSON의 credential 필드 검사 → re-import 후 로그인 검증 | `needs-confirmation` |
| 환경 reset (`docker compose down -v && up`) 후 realm/client/user 자동 import 동작 | 위 D5 가 `needs-confirmation` — 실제 동작 검증 미수행 | docker-compose 구동 → admin 로그인 → realm `keycloak-patterns` 존재 + `spa-client` 존재 확인 | `planned` |
| realm-export.json 의 client PKCE 내부 속성 key 가 `pkce.code.challenge.method` 인지 | `KC-PKCE-C1` "Does not prove" — page 에 내부 속성명 미명시 | Admin UI 에서 "PKCE method=S256" 설정 후 `kc.sh export` → 생성된 JSON 의 `clients[].attributes` key 확인 | `needs-confirmation` |
## Audit & Findings
> `/branch-spec` 채움 중 발견한 정합/근거 이슈. 사용자 작성 결정 영역은 자동 rewrite 하지 않고 정합 권고만 기록(CLAUDE.md §11, consistency-contract).
- **`EVIDENCE_UPGRADE`** — 노트 최초 작성(2026-05-25) 당시 D1/D2/D5 는 근거 raw 부재로 전부 `UNSUPPORTED_DECISION` 이었다. 이후 corpus 성장으로 `oauth2-pkce-rfc-7636`, `keycloak-client-pkce-method-enforcement-official`, `oauth2-browser-based-apps-ietf-draft`, `oauth-v2-1-draft-ietf`, `keycloak-getting-started-docker`, `keycloak-import-export-realms` 가 추가되어 official 근거로 승격. **재조사(researcher dispatch) 없이 기존 raw 재매핑으로 해결** — 모든 결정이 근거 보유 또는 정당한 UNSUPPORTED trade-off(D4).
- **`NAMING_DRIFT` (해소 2026-07-18)** — §목표·§TODO·§Claims를 대상 UI의 **`PKCE method`**(Capability config)로 통일했다. 다른 Keycloak 버전의 라벨·내부 key 차이는 생성된 realm export와 대조하며, exact 거부 응답은 `needs-confirmation`으로 유지한다.
- **`OWNERSHIP_NARROWED` (D5)** — 원 D5는 "export commit + `--import-realm` 자동 import"를 함께 기술했으나, `--import-realm` + volume mount 배선은 sibling [[raw/branch-notes/feature-keycloak-docker-compose-stack]] D4가 owner다. 본 D5는 realm-export.json 저작·commit과 consumer acceptance로 좁혔다.
- **`DELEGATED_RATIONALE` (D3/D4)** — rotation 값·정책은 [[raw/branch-notes/feature-keycloak-refresh-token-rotation]] D1/D2, 실행·관찰은 [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] D1/D5가 owner다. 본 branch는 target-version 결과로 확정된 값을 realm-export.json에 기록할 뿐 `Max Reuse`나 family invalidation 의미를 재진술하지 않는다.
- **`NO_GROUND_TRUTH` (code)** — 실 구현 repo `/home/donghyeon/workspace/keycloak-patterns/` **부재** 확인. 코드 grep 으로 `actually-implemented` 확정 불가 → 모든 항목 `planned`/`documented-only` 유지. (ca-tmpl ground truth 는 본 keycloak-patterns 프로젝트에 비적용 — 별개 트리)
## 마주친 문제
- (구현 시작 후 추가) realm export JSON 안에 client secret이 평문으로 들어가는 경우 — gitignore 또는 redact 필요.
- (구현 시작 후 추가) user password 재설정 자동화 어려움 — 초기 password 정책 / temporary password flag 활용 검토.
## 묶음
<!-- GENERATED: sources:start -->
- [[raw/official-docs/keycloak-getting-started-docker]]
<!-- GENERATED: sources:end -->
> 본 sub-sub-branch 는 leaf — 자식 자료 없음. Phase 3 P3A 실 구현 또는 외부 산출물 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
### 오류 기록 (이 sub-sub-branch 작업 중 발생)
- (없음 — 현재 documented-only 단계)
### 면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- (없음 — Phase 3 실 구현 단계에 누적)
## 관련 일일 노트
## 완료 후 정리
> 실 구현 후 realm import 자동화 검증 시 `planned` → `actually-implemented`/`locally-verified` 승급.
- PR 링크: (별도 keycloak-patterns repo)
- 리뷰 메모:
- 머지 결과 / 배포 환경: 로컬 docker-compose / 단일 EC2 시뮬레이션
- **wiki 추출 대상**:
- `actually-implemented` 항목: (구현 후 채움)
- `locally-verified` 항목: (구현 후 채움)
- `prod-verified` 항목: (없음)
- **추출하지 않을 항목**: 현재 전부 `planned`.