# Lane 3 — Keycloak Branch-Note Consistency Audit ## Lane Inventory | Path | Status | Evidence Lines | Extracted Facts | | --- | --- | --- | --- | | `raw/branch-notes/feature-keycloak-nginx-auth-request-integration.md` | READ_FULL | 1–306 | `auth_request` body handling, browser/API failure routing, upstream ownership edges | | `raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow.md` | READ_FULL | 1–275 | bearer-token terminology, nginx decision references, discovery startup dependency | | `raw/branch-notes/feature-keycloak-patterns.md` | READ_FULL | 1–205 | retired numbered naming versus retained D2 | | `raw/branch-notes/feature-keycloak-pkce-flow-stages.md` | READ_FULL | 1–167 | PKCE S256 ownership and Admin Console terminology | | `raw/branch-notes/feature-keycloak-public-domain-tunneling.md` | READ_FULL | 1–220 | quick versus named Cloudflare Tunnel domains and tunnel-choice ownership | | `raw/branch-notes/feature-keycloak-realm-client-export.md` | READ_FULL | 1–238 | import wiring ownership, credential export assertions, PKCE terminology | | `raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md` | READ_FULL | 1–254 | rotation demonstration, Max Reuse semantics, back-channel logout scope | | `raw/branch-notes/feature-keycloak-refresh-token-rotation.md` | READ_FULL | 1–284 | rotation contract, family invalidation, receiving endpoint delegation | | `raw/branch-notes/feature-keycloak-reverse-proxy-headers.md` | READ_FULL | 1–297 | Caddy path behavior, Keycloak proxy variables, delegated security decisions | | `raw/branch-notes/feature-keycloak-single-ec2-google-federation.md` | READ_FULL | 1–370 | parent integration decisions and delegated tunnel/proxy details | ## 1. `feature-keycloak-nginx-auth-request-integration.md` ### L3-F01: `auth_request` body behavior contradicts its configuration table - **Severity / class:** High — `CONTRADICTION` - **Original goal / current state:** line 101 says the body is not automatically copied; line 163 says nginx defaults to forwarding it. - **Claim:** Both statements cannot govern the same configuration. - **Assumption:** `proxy_pass_request_body off` can be omitted in the rendered config. This is falsified if every applicable location explicitly contains it. - **Failure / falsification:** Request bodies reach the auth upstream, causing unwanted disclosure, buffering, or size failures. Falsify with `nginx -T` and upstream request capture. - **Action / why:** Rewrite line 101 to say body forwarding must be disabled explicitly, and make the table canonical. - **Alternatives:** (1) explicit `off`, (2) intentional bounded forwarding with documented limits, (3) a separate body-less auth endpoint. - **Counterargument:** oauth2-proxy may ignore the body. That does not establish that nginx did not transmit it. ```text $ sed -n '101p;163p' 'raw/branch-notes/feature-keycloak-nginx-auth-request-integration.md' - nginx `auth_request`는 subrequest이므로 **POST body가 자동 복제되지 않는다**. `/oauth2/auth`는 HEAD-like check이므로 body 불필요 → `proxy_pass_request_body off` 권장. | `proxy_pass_request_body` | `off` | `O2PN-C8`, `NGAR-C8`. **생략하면 default `on`(`NGXPM-C1`) 이라 body 가 전달됨** | ``` ### L3-F02: unconditional sign-in TODO is stale after browser/API response split - **Severity / class:** Medium — `STALE_SUMMARY` - **Original goal / current state:** line 89 gives one 302 sign-in task; lines 193–194 later divide browser 302 from API 401. - **Claim:** The implementation checklist can be read as applying the redirect to every protected location. - **Assumption:** Implementers use the TODO before the later D9 table. Falsified if generated config and tests always preserve the location split. - **Failure / falsification:** API clients receive HTML redirects, parsing failures, or loops. Verify both browser and `/api/` requests. - **Action / why:** Restrict the TODO to browser locations and add an explicit API 401 task. - **Alternatives:** (1) location split, (2) 401 everywhere with frontend login handling, (3) redirect allowlist, (4) content-negotiated behavior. - **Counterargument:** The TODO may describe only creation of the named location. Its application scope nevertheless remains unstated there. ```text $ sed -n '89p;193p;194p' 'raw/branch-notes/feature-keycloak-nginx-auth-request-integration.md' - [ ] named location `@oauth2_signin` 작성: `return 302 https://$host/oauth2/start?rd=$scheme://$host$request_uri;` — 등급: `planned` | browser-facing (`location /`) | `auth_request /oauth2/auth;` | `error_page 401 = @oauth2_signin;` → 302 | D2, `O2PN-C5` | | API/machine (`location /api/`) | `auth_request /oauth2/auth;` | `error_page 401 =401;` → **plain 401 pass-through** | D9, `O2PN-C9` | ``` ## 2. `feature-keycloak-oauth2-proxy-oidc-flow.md` ### L3-F03: locally validated JWT behavior remains labeled “token introspection” - **Severity / class:** Low — `STALE_SUMMARY`, `NAMING_DRIFT` - **Original goal / current state:** the TODO calls the flags an introspection mode, while its own audit says no introspection endpoint call is documented. - **Claim:** The term implies a network protocol behavior not supported by the note’s evidence. - **Assumption:** Readers use the label to infer latency, revocation, and firewall dependencies. Falsified by an observed outbound introspection request. - **Failure / falsification:** Architecture is designed around an endpoint call that may not happen. Verify debug logs and outbound traffic. - **Action / why:** Use neutral “bearer-token mode” or the verified local-validation/bypass behavior. - **Alternatives:** (1) local JWT validation, (2) bearer-token bypass, (3) separately implement RFC 7662 introspection, (4) retain a neutral label pending measurement. - **Counterargument:** “Introspection” might be informal internal vocabulary, but it conflicts with the protocol’s established meaning. ```text $ sed -n '85p;101p' 'raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow.md' - [ ] token introspection 모드 (`--skip-jwt-bearer-tokens`, `--extra-jwt-issuers`)의 의미 정리 — 등급: `planned` - **2026-07-17** (drift): 본 노트가 쓰는 **"token introspection 모드" 명칭이 부정확할 가능성**을 확인 — 공식 문서는 introspection endpoint 호출을 서술하지 않음. 사용자 작성 본문이라 자동 수정하지 않고 §Audit & Findings `NAMING_DRIFT` 로 남김. rename 여부는 사용자 판단. ``` ### L3-F04: three decision references are not machine-resolvable - **Severity / class:** Low — `BARE_DECISION_REF`, ambiguous ownership - **Original goal / current state:** all three passages intend to identify an owner decision but use bare slugs or quoted prose instead of the repository’s decision-reference syntax. - **Claim:** Impact analysis cannot reliably bind these references to their owners. - **Assumption:** Deterministic consistency tooling is part of change propagation. Falsified only if the tool and contract are changed to recognize these forms. - **Failure / falsification:** Owner changes do not surface in consumer impact reports. Re-run the consistency checker after repair. - **Action / why:** Replace each with `[[raw/branch-notes/]] D`. - **Alternatives:** N/A — reference syntax is fixed by `rules/consistency-contract.md`. - **Counterargument:** A person can understand the references, but that does not make them machine-traceable. ```text $ sed -n '214p;228p' 'raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow.md' | `SESSION_STORAGE_4KB_ABSENT` | session storage 공식 페이지에 `4k`/`4096`/`split` 문자열이 **0건** — 4kb cookie split 함정의 근거는 이 페이지가 아니라 **nginx 통합 페이지**([[raw/official-docs/oauth2-proxy-nginx-integration-official]], "Nginx normally only copies the first `Set-Cookie` header ... if your cookies are larger than 4kb, you will need to extract additional cookies manually")에 있다. | `raw/official-docs/oauth2-proxy-session-storage-official.md` §Usage Boundaries | D5a 의 Open Risk 에 반영 완료. 4kb 대응의 owner 는 형제 branch `feature-keycloak-nginx-auth-request-integration` `D5`. | | **인가 거부 시 실제 응답 코드/본문** (401 vs 403 vs 로그인 루프) | D4 Open Risk 가 자인 — `O2PK-C3` 은 인가 실패 코드의 의미를 명시 안 하고, `O2PBEH-C2`~`C4` 는 authentication 단계 전용. 형제 branch `feature-keycloak-nginx-auth-request-integration` `D2` 의 `error_page` 분기가 이 값에 의존 | 허용 role 이 **없는** 사용자로 로그인 후 보호 경로 요청 → 응답 코드/본문 확인. `/oauth2/auth` subrequest 응답도 함께 확인(202/401 만 반환하는지 — `O2PE-C5` 와 대조) | `needs-confirmation` | ``` ```text $ sed -n '182p' 'raw/branch-notes/feature-keycloak-realm-client-export.md' - [[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/포트를 바꾸면 본 realm-export.json 의 redirect URI(D2) 를 동기화해야 함. (sibling `feature-keycloak-docker-compose-stack` 도 동일 의존을 "parent D3" 로 인용). 아울러 본 branch 의 client scope(realm 1개 + `spa-client` public + PKCE S256) 는 parent `#D5` 의 실 구현. ``` ## 3. `feature-keycloak-patterns.md` ### L3-F05: retired numbered naming remains an active D2 decision - **Severity / class:** Medium — `CONTRADICTION`, `STALE_SUMMARY` - **Original goal / current state:** line 60 retires numbered naming; line 134 still presents it as D2. - **Claim:** A prohibited convention remains encoded as an active-looking decision. - **Assumption:** D-rows denote current decisions. Falsified if D2 is structurally marked historical and excluded from owner resolution. - **Failure / falsification:** New branches or consumers adopt the retired numbered hierarchy. Search inbound D2 references. - **Action / why:** Move D2 to a retired-decision section or apply an explicit retired status. - **Alternatives:** N/A — numbered branch hierarchy is prohibited by repository policy. - **Counterargument:** The warning at line 60 is visible, but downstream D-reference semantics remain ambiguous. - **Single-finding justification:** Other content is legacy-to-AP mapping; no second lane-local decision conflict was found. ```text $ sed -n '60p;134p' 'raw/branch-notes/feature-keycloak-patterns.md' > **⚠️ 갱신 (2026-07-14)**: 아래 6패턴 인덱스는 **Phase 0 legacy(배치×federation 축)**. 현 실행계획 SSOT 는 [[raw/project-notes/keycloak-patterns-overview]] 의 **§Branch 분해 / 실행계획(R4)** — 인증 아키텍처 4패턴(AP1~AP4) + 19 Tier-2. 신규 작업은 hub 분해표를 따르며, 아래 슬러그는 hub §2.3 매핑대로 AP 로 re-map 대상. D2(`-{N}-{M}` numbered 명명)는 CLAUDE.md §11 위반으로 폐기(각 sub-sub 는 이미 content-descriptive 슬러그라 실제 영향은 프레이밍뿐). - **D2** 2026-05-25: sub-sub-branch 명명은 `-{N}-{M}` dash-숫자 형식. M은 학습/구현 단계 순서. ``` ## 4. `feature-keycloak-pkce-flow-stages.md` ### L3-F06: D1 and D5 both own S256 enforcement - **Severity / class:** Medium — `DUAL_OWNERSHIP` - **Original goal / current state:** D1 and D5 both encode the S256 formula and Keycloak enforcement, with different evidence freshness. - **Claim:** Two decisions own the same mutable contract. - **Assumption:** Downstream notes treat a D-number as owner identity. Falsified if D5 is structurally non-normative. - **Failure / falsification:** UI-label or rejection-semantics updates reach only one row. Compare inbound D1/D5 references. - **Action / why:** Keep D1 canonical and turn D5 into a reference-only TODO mapping. - **Alternatives:** (1) D1 owner/D5 pointer, (2) D5 owner/D1 scope summary, (3) merge rows, (4) split formula from vendor enforcement. - **Counterargument:** D5 may exist only to map TODO step 2, but its current table form still looks normative. ```text $ sed -n '118p;122p' 'raw/branch-notes/feature-keycloak-pkce-flow-stages.md' | D1 | PKCE method = `S256` 만 정리 대상 (`plain` 은 비교용 1줄). Keycloak client 설정에서 PKCE method 옵션(정식 UI 라벨 "PKCE method")을 S256 으로 지정 | `raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C3` (S256 공식: `BASE64URL-ENCODE(SHA256(ASCII(verifier)))`), `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C1` (OAuth 2.1: "Clients MUST use code_challenge and code_verifier ..."), `raw/official-docs/keycloak-client-pkce-method-enforcement-official.md#KC-PKCE-C1` (Admin UI 옵션 정식 명칭·위치), `#KC-PKCE-C3` (S256 선택 시 서술) | `official-standard + official-standard + official-vendor-doc` | OA21-C1 은 PKCE 사용 자체를 MUST 로 강제하지만 "S256 강제 / plain 금지" 라는 정확한 문장은 OA21-C1 인용에 포함 안 됨 — §7.5.1 예외 조건 확인 필요. 단, RFC 7636 + OAuth 2.1 종합 권고로 보면 정당. **2026-07-17 업데이트**: `KC-PKCE-C1` 이 UI 라벨 오류를 정정("Proof Key for Code Exchange Code Challenge Method" 가 아니라 "PKCE method", Capability Config 섹션)했으나, `KC-PKCE-C3` 은 "Keycloak applies... S256" 이라고만 서술 — **S256 설정 시 `code_challenge_method=plain` 요청을 실제로 거부(reject)한다는 명시적 문장은 여전히 없음**. 아래 Claims To Verify 의 "plain 메서드 요청을 거부" 항목은 `needs-confirmation` 유지 | | D5 (TODO 표 step 2) | S256 공식 `code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))` + Keycloak client 에 S256 강제 | `raw/official-docs/oauth2-pkce-rfc-7636.md#PKCE-RFC7636-C3` (S256 공식), `raw/official-docs/keycloak-securing-apps-overview-official.md#KC-SECAPP-C1` (Keycloak 이 OIDC/OAuth 2.0 stack 을 secure 가능) | `official-standard` + `official-vendor-doc` (Keycloak overview) | Keycloak client 의 "Proof Key for Code Exchange Code Challenge Method = S256" 정확한 admin UI 위치는 KC-SECAPP-C1 인용 범위 밖 — Keycloak Securing Applications 의 client adapter / settings 섹션 별도 인용 필요 | ``` ### L3-F07: deprecated PKCE Admin label survives in two TODOs - **Severity / class:** Low — `STALE_SUMMARY` - **Original goal / current state:** two implementation tasks use the old label; the audit identifies `PKCE method` as the actual label. - **Claim:** The correction has not reached the operational checklist. - **Assumption:** The pinned Keycloak version uses the audited label. Falsified by the target Admin Console displaying the older text. - **Failure / falsification:** Implementers cannot find or select the intended field. Inspect the target-version UI. - **Action / why:** Update both TODOs to `PKCE method = S256`; preserve the old label only as version history. - **Alternatives:** N/A — use the label observed in the pinned target version. - **Counterargument:** Labels can vary by Keycloak version or locale; record that variant explicitly if confirmed. ```text $ sed -n '73p' 'raw/branch-notes/feature-keycloak-pkce-flow-stages.md' - Keycloak client 설정: `Proof Key for Code Exchange Code Challenge Method = S256` 강제 ``` ```text $ sed -n '81p;202p' 'raw/branch-notes/feature-keycloak-realm-client-export.md' - [ ] Client Advanced 탭: `Proof Key for Code Exchange Code Challenge Method = S256` — 등급: `planned` - **`NAMING_DRIFT`** (Advisory, user prose 보존) — §목표·§TODO 의 라벨 "Proof Key for Code Exchange Code Challenge Method" 는 `KC-PKCE-C1` 기준 부정확. 실제 Admin Console 옵션명은 **"PKCE method"** (Capability Config 섹션 하위). 구현 시 이 라벨로 찾을 것. 사용자 작성 prose 는 verbatim 보존, 정정은 §구현 가이드 1 에 반영. ``` ## 5. `feature-keycloak-public-domain-tunneling.md` ### L3-F08: `trycloudflare.com` is described as both permanent and random - **Severity / class:** High — `CONTRADICTION`, `STALE_SUMMARY` - **Original goal / current state:** line 69 offers a permanent `.trycloudflare.com`; line 77 says quick-tunnel URLs are random, while the parent repeats the static-looking form. - **Claim:** Quick-tunnel hostnames and named-tunnel managed DNS are conflated. - **Assumption:** Google requires a stable registered redirect and the described quick-tunnel behavior applies. Falsified by an account providing a persistent named `trycloudflare.com` hostname. - **Failure / falsification:** Restarting the tunnel invalidates the Google redirect URI. Create/restart the tunnel and compare hostnames. - **Action / why:** Distinguish random quick tunnels from named tunnels backed by managed DNS; synchronize the parent summary. - **Alternatives:** (1) random URL plus console updates, (2) named tunnel plus managed domain, (3) ngrok reserved domain, (4) another stable public endpoint. - **Counterargument:** Product capabilities may have changed; verify against the exact account and client version. ```text $ sed -n '69p;77p' 'raw/branch-notes/feature-keycloak-public-domain-tunneling.md' - [ ] **Cloudflare Tunnel 동작 확인** — `cloudflared tunnel create ` + `cloudflared tunnel route dns ` → 영구 도메인 (`.trycloudflare.com` 무료 또는 자체 도메인 CNAME) — 등급: `planned` - Cloudflare Tunnel의 `trycloudflare.com` quick tunnel은 무료지만 URL이 random (ngrok와 유사). 정적 도메인 원하면 Cloudflare account + 자체 도메인 (Cloudflare DNS로 위임) + named tunnel 필요. ``` ```text $ sed -n '356p' 'raw/branch-notes/feature-keycloak-single-ec2-google-federation.md' - **비교 핵심**: P3A 대비 **추가 운영 비용은 "public 접근 가능한 callback URL"** 1가지. Google OAuth client console에 등록할 redirect_uri는 **HTTPS 필수 + localhost 제외** (Google 명시 — `http://localhost` 만 예외, IP 주소 X). 학습 환경에서는 **ngrok** (`https://.ngrok-free.app` 임시 URL) 또는 **Cloudflare Tunnel** (`.trycloudflare.com` 또는 자체 도메인) 사용. ngrok은 무료 plan에서 URL이 매 세션 변경 → Google client console 매번 업데이트 필요 → Cloudflare Tunnel이 학습 단계에 더 적합 (정적 도메인). **Keycloak `KC_PROXY_HEADERS=xforwarded` + `KC_HOSTNAME=`** 명시 안 하면 reverse proxy 뒤에서 redirect URL이 internal `keycloak:8080`으로 떨어져 깨짐. ``` This file is also the child-side source for `L3-F15`. ## 6. `feature-keycloak-realm-client-export.md` ### L3-F09: import wiring remains in scope after ownership was delegated - **Severity / class:** Medium — `STALE_SUMMARY`, `RESTATED_FOREIGN_DECISION` - **Original goal / current state:** line 53 includes automatic container import; line 168 says this branch authors JSON only and must not restate wiring. - **Claim:** Scope and ownership trace assign the same concern differently. - **Assumption:** Scope bullets denote implementation responsibility. Falsified if line 53 is explicitly reclassified as a consumer assumption. - **Failure / falsification:** Realm and stack branches independently change mount paths or startup flags. Inspect actual changed artifacts. - **Action / why:** Keep JSON authorship here and make import wiring reference-only to the stack owner. - **Alternatives:** (1) JSON/stack split, (2) this branch owns full import lifecycle, (3) dedicated import-lifecycle owner. - **Counterargument:** Line 53 might describe desired behavior, not ownership; its current placement does not state that distinction. ```text $ sed -n '53p;168p' 'raw/branch-notes/feature-keycloak-realm-client-export.md' - Keycloak 컨테이너 `--import-realm` 옵션으로 부트 시 자동 import > **Trace / R3**: volume mount(`./realm-export.json:/opt/keycloak/data/import/realm-export.json`) + 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 저작만** — 배선은 restate 하지 않는다. 배선 계약이 바뀌면(import dir 경로/flag) 본 export 파일 배치에 영향(§엣지·의존). ``` ### L3-F10: credential export is asserted before its evidence gate - **Severity / class:** High — `CONTRADICTION`, unsupported certainty - **Original goal / current state:** line 99 states credential behavior; line 193 says the password-inclusive `usersExport` claim has no primary raw source. - **Claim:** Operational prose presents an unverified export mode as available. - **Assumption:** Bootstrap automation may rely on exported users or passwords. Falsified by version-matched documentation and inspected export output. - **Failure / falsification:** Imported users cannot authenticate, or sensitive credentials are handled under a false assumption. - **Action / why:** Move credential claims to verification status until the pinned image’s help and actual export are inspected. - **Alternatives:** (1) no users and separate bootstrap, (2) verified user export with secret controls, (3) Admin API provisioning, (4) disposable test credentials. - **Counterargument:** The behavior may be remembered from another Keycloak version; that does not establish the pinned version. ```text $ sed -n '99p;193p' 'raw/branch-notes/feature-keycloak-realm-client-export.md' - Realm export JSON은 **user password를 포함하지 않음** (보안). user는 ID/role만 export됨 → 부트 후 admin이 password 재설정 필요. 또는 realm export에서 `usersExport=true`로 별도 export 가능. | `usersExport=true` 옵션으로 user (password 포함) export 가능 | 본 branch 진행 중 메모 — 1차 raw 미수집 | Keycloak `kc.sh export --help` 또는 admin REST API 문서 발췌 | `planned` | ``` This file is also implicated in `L3-F04` and `L3-F07`. ## 7. `feature-keycloak-refresh-rotation-and-logout.md` ### L3-F11: concept and execution notes both own the same rotation sequence - **Severity / class:** High — `DUAL_OWNERSHIP`, `RESTATED_FOREIGN_DECISION` - **Original goal / current state:** the concept note declares a concept/execution split, but both notes retain a normative D-row for the same family-invalidation flow. - **Claim:** Mutable expected behavior has two decision owners. - **Assumption:** D-rows are normative. Falsified if P3A D5 is structurally converted into a non-normative test procedure. - **Failure / falsification:** A failed family-kill test updates only one note. Inspect incoming D4/D5 references and modification ownership. - **Action / why:** Keep policy/expected behavior in the concept owner and make P3A reference it from its test procedure. - **Alternatives:** (1) concept owner/execution pointer, (2) execution owner/concept principle, (3) explicit invariant/procedure split, (4) separate verified-behavior owner. - **Counterargument:** Definition and demonstration are naturally repeated, but both current rows make the repeated value independently mutable. ```text $ sed -n '159p;240p' 'raw/branch-notes/feature-keycloak-refresh-token-rotation.md' | D4 | rotation flow 4단계 (RT_1 사용 → invalidate → AT_2+RT_2 발급 → 재사용 시 family invalidate) | N/A — rotation ON 이면 항상 이 flow. 단 재사용 시 무효화 **범위**(단일 토큰 vs family 전체)는 verify 대상 | `raw/official-docs/keycloak-refresh-token-rotation-sessions-official.md#KC-RTROT-C2` (RT 1회 사용 후 무효 + 최신 RT 저장 강제 = flow 1~3단계 근거) + `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3` (refresh token = scope/RS bound MUST, 배경) + `UNSUPPORTED_DECISION` (4단계 "family 전체 invalidate" 는 `KC-RTROT-C6` doc text 부재) | `official-vendor-doc (1~3단계) + official-standard (배경) + UNSUPPORTED_DECISION (family invalidate)` | family invalidate 동작을 docker-compose 실측(RT_1 재사용 → RT_2 도 거부되는지)으로 확인 — Claims To Verify 에 계획됨 | - cousin(다른 phase) [[raw/branch-notes/feature-keycloak-refresh-rotation-and-logout]] (P3A) — 본 개념 계약의 **실측·시연** 소유. 본 노트 = 개념/계약, 그쪽 = 실행. ``` ```text $ sed -n '124p' 'raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md' | D5 | rotation flow 시연 (RT_1 사용 → AT_2+RT_2 → RT_1 재사용 → family 전체 invalidate → 재로그인 강제) | N/A (rotation ON 전제의 단일 시연 시나리오). rotation OFF 면 재사용 탐지 자체가 불가 → D1 이 전제 | `raw/official-docs/keycloak-refresh-token-rotation-reuse-admin-official.md#KC-ROT-C1` (rotation = 사용된 RT 무효화 + 새 발급) + `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3`·`#OA21-C6` (RT bound MUST, code grant → AT+RT) + `raw/official-docs/oidc-client-ts-library.md#OIDCTS-C4`·`#OIDCTS-C5` (Refresh Token Grant + Silent Refresh — SPA 자동 갱신 경로) + `UNSUPPORTED_DECISION` ("RT_1 재사용 → family 전체 invalidate" 자동 동작 = `KC-ROT-C6` 문서 부재) | `official-vendor-doc (KC-ROT-C1, OIDCTS-C4/C5) + official-standard (OA21-C3/C6) + UNSUPPORTED_DECISION (family invalidate 구체 동작)` | docker-compose 실측: RT 재사용 시 4xx + RT_2 동시 invalidate(family kill) 확인 (Claims To Verify #1) | ``` ### L3-F12: delegated back-channel receiver is out of scope at its destination - **Severity / class:** High — `CONTRADICTION`, ambiguous ownership - **Original goal / current state:** the concept note delegates receiver implementation to P3A; P3A lists provider-triggered logout as an optional demonstration only. - **Claim:** No note unambiguously owns the receiving endpoint. - **Assumption:** “옵션 시연만” excludes backend implementation. Falsified by an explicit endpoint artifact and validation plan in P3A. - **Failure / falsification:** Logout Token reception is relied upon but never built or verified. - **Action / why:** Add the endpoint to P3A, remove the delegation, or create a dedicated owner. - **Alternatives:** (1) implement in P3A, (2) remove from scope, (3) dedicated branch, (4) provider-side demonstration with explicit absence of receiver. - **Counterargument:** The optional demonstration might include the endpoint; planned files and acceptance tests must show that. ```text $ sed -n '67p' 'raw/branch-notes/feature-keycloak-refresh-token-rotation.md' - **back-channel logout 수신** backend 구현(Logout Token 수신 endpoint) — D3, P3A 실 구현 시 ``` ```text $ sed -n '61p' 'raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md' - back-channel logout (Keycloak admin → 모든 session kill) — 옵션 시연만 ``` ### L3-F13: Max Reuse greater than zero is both meaningless and a weaker alternative - **Severity / class:** High — `CONTRADICTION`, incompatible policy assumptions - **Original goal / current state:** one note says any value above zero makes reuse detection meaningless; the other treats one or more as weaker but viable. - **Claim:** The policy models the setting with incompatible semantics. - **Assumption:** Max Reuse is a bounded-use counter in the pinned version. If the field differs or is absent, both claims need replacement. - **Failure / falsification:** Concurrency mitigation either disables the intended test or rejects legitimate refreshes. Test values 0 and 1 with concurrent requests. - **Action / why:** Replace certainty with one measured semantics table shared by both notes. - **Alternatives:** (1) zero plus refresh serialization, (2) measured bounded reuse, (3) BFF/server-side serialization, (4) defer selection until measurement. - **Counterargument:** “의미 없음” may mean only “unsuitable for D5”; the note does not currently state that narrower scope. ```text $ sed -n '127p' 'raw/branch-notes/feature-keycloak-refresh-token-rotation.md' - `Refresh Token Max Reuse > 0`이면 reuse detection 의미 없음 ``` ```text $ sed -n '120p' 'raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md' | D1 | rotation ON + Max Reuse 0 강제 — stolen token 탐지 시연 | stolen-token 탐지를 *시연*하려면 rotation ON + Max Reuse 0. 정상 SPA 가 동시 갱신(race)으로 같은 RT 를 2회 보낼 위험이 크면 대안: Max Reuse ≥1 로 완화(단 탐지력 약화, D5 시연 불가) | `raw/official-docs/keycloak-refresh-token-rotation-reuse-admin-official.md#KC-ROT-C1` ("Revoke Refresh Token" ON = 사용된 RT revoke + 새 토큰 발급 = rotation) + `raw/official-docs/oauth-v2-1-draft-ietf.md#OA21-C3` (RT scope/RS bound MUST — rotation 권고 배경) + `UNSUPPORTED_DECISION` ("Refresh Token Max Reuse" 설정명 + 재사용 시 family invalidate 자동 동작 = `KC-ROT-C6` 문서 부재) | `official-vendor-doc (KC-ROT-C1) + official-standard (OA21-C3 배경) + UNSUPPORTED_DECISION (Max Reuse 0 라벨 + family invalidate)` | admin UI 실측으로 "Refresh Token Max Reuse" 필드 실재·라벨 + RT 재사용 시 family kill 동작 확인 (Claims To Verify #1) | ``` ## 8. `feature-keycloak-refresh-token-rotation.md` This file is the co-source for `L3-F11`, `L3-F12`, and `L3-F13`. No additional lane-local inconsistency remained after those shared findings; its remaining related D-rows retain explicit `UNSUPPORTED_DECISION` labels rather than asserting family invalidation as verified. ## 9. `feature-keycloak-reverse-proxy-headers.md` ### L3-F14: executable Caddy draft strips a prefix the selected method requires - **Severity / class:** High — `STALE_SUMMARY`, cannot-coexist configuration - **Original goal / current state:** the copyable draft uses `handle_path`; its later audit says method B requires prefix-preserving `handle`/`reverse_proxy`. - **Claim:** The executable draft and `KC_HTTP_RELATIVE_PATH=/keycloak` cannot coexist without another rewrite. - **Assumption:** Standard `handle_path` stripping applies and Keycloak runs at `/keycloak`. Falsified by a verified rewrite or root-path Keycloak configuration. - **Failure / falsification:** Discovery and authorization endpoints reach the wrong path and return 404. Test all OIDC endpoints through Caddy. - **Action / why:** Repair the snippet itself or explicitly switch the design to method A. - **Alternatives:** (1) prefix-preserving `handle`, (2) method A with deliberate stripping, (3) explicit verified rewrite, (4) nginx prefix preservation. - **Counterargument:** Another layer may restore the prefix; no such layer is shown in the draft. ```text $ sed -n '125p;202p' 'raw/branch-notes/feature-keycloak-reverse-proxy-headers.md' handle_path /keycloak/* { > ⚠️ **method B ↔ proxy 라우팅 정합 함정 (depth 감사 2026-07-18 F1)**: §진행 중 메모의 **Caddy 초안 `handle_path /keycloak/*`** 은 `/keycloak` prefix 를 **strip** 한다 (= method A 동작). 그런데 D3 은 method B(`KC_HTTP_RELATIVE_PATH=/keycloak`, Keycloak 이 스스로 `/keycloak/*` 발급)를 채택했으므로, prefix 가 strip 된 요청이 prefix 를 기대하는 Keycloak 에 도달 → **이중 strip 으로 404**. 초안의 "Caddy 환경변수는 nginx case 와 동일" 은 *환경변수*는 맞으나 *라우팅*은 다름을 가린다. nginx `location /keycloak/ { proxy_pass http://127.0.0.1:8080; }` 는 prefix 를 **보존**하므로 method B 와 정합. **method B 를 Caddy 로 쓰려면 `handle_path`(strip) 대신 `handle`/`reverse_proxy` 로 `/keycloak` prefix 를 보존**해야 한다. Caddy vendor doc 미보존(D4 UNSUPPORTED)이라 FACT 단정 대신 §Claims To Verify 로 실측 위임. ``` ### L3-F16: parent owns exact proxy values while delegating proxy details - **Severity / class:** High — `DUAL_OWNERSHIP`, `RESTATED_FOREIGN_DECISION` - **Original goal / current state:** parent D4 fixes five exact variables; its ownership summary delegates proxy-header detail to the child. - **Claim:** The parent contains a mutable configuration bundle that overlaps delegated owners. - **Assumption:** Parent D4 is consumed as normative configuration. Falsified if it is generated from one canonical source and children cease owning values. - **Failure / falsification:** A routing or security owner changes a value while the parent remains deployable and stale. - **Action / why:** Keep integration invariants in the parent and reference each variable’s owner decision. - **Alternatives:** (1) child owners plus parent references, (2) parent owns final bundle and children provide evidence, (3) explicit per-variable ownership matrix. - **Counterargument:** An integration parent may need the assembled bundle, but then it must be the declared canonical owner rather than also delegating details. ```text $ sed -n '239p;301p' 'raw/branch-notes/feature-keycloak-single-ec2-google-federation.md' | D4 | Keycloak `KC_HOSTNAME=https://kc.example.com` + `KC_HTTP_RELATIVE_PATH=/keycloak` + `KC_PROXY_HEADERS=xforwarded` + `KC_HTTP_ENABLED=true` + `KC_PROXY_TRUSTED_ADDRESSES=127.0.0.1` | `raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C2`, `raw/official-docs/keycloak-hostname-configuration.md#KC-HOST-C5`, `raw/official-docs/keycloak-reverseproxy-official.md#KC-RP-C2`, `raw/official-docs/keycloak-reverseproxy-official.md#KC-RP-C3`, `raw/official-docs/keycloak-reverseproxy-official.md#KC-RP-C4`, `raw/official-docs/keycloak-reverseproxy-official.md#KC-RP-C5`, `raw/official-docs/keycloak-reverseproxy-official.md#KC-RP-C6` | `official-vendor-doc` | `KC_HTTP_RELATIVE_PATH=/keycloak` 와 `X-Forwarded-Prefix` 가 nginx → Keycloak 경로에서 OIDC discovery (`.well-known/openid-configuration`) 의 `issuer` 값을 어떻게 결정하는지 실 검증 필요 (`KC-RP-C6` 두 방법의 trade-off 미해소) | - sub-sub-branch 관심사 위임: [[raw/branch-notes/feature-keycloak-public-domain-tunneling]](tunnel 상세) · [[raw/branch-notes/feature-keycloak-reverse-proxy-headers]](proxy header 상세) · [[raw/branch-notes/feature-keycloak-google-redirect-uri-policy]](redirect URI 정책) · [[raw/branch-notes/feature-keycloak-https-termination-caddy-nginx]](TLS termination) 가 각 관심사 detail owner. ``` ## 10. `feature-keycloak-single-ec2-google-federation.md` ### L3-F15: parent and child both decide Cloudflare-over-ngrok - **Severity / class:** Medium — `DUAL_OWNERSHIP` - **Original goal / current state:** parent D3 and child D1 both own the same provider preference, while the child supplies the selection conditions. - **Claim:** Tunnel-selection authority is duplicated. - **Assumption:** Both D-rows are normative and independently mutable. Falsified by a declared selection/detail split without restating the mutable preference. - **Failure / falsification:** A provider-policy change updates only one branch. Inspect inbound D3/D1 references and approval ownership. - **Action / why:** Make the child the selection owner and reduce the parent to required outcomes plus a decision reference. - **Alternatives:** (1) child owns selection, (2) parent owns selection and child operationalizes, (3) parent owns requirements while child maps conditions to providers. - **Counterargument:** Parent selection plus child implementation can be valid, but both current rows phrase the provider preference as their own decision. ```text $ sed -n '238p' 'raw/branch-notes/feature-keycloak-single-ec2-google-federation.md' | D3 | 개발용 public URL 노출 수단은 Cloudflare Tunnel 우선, ngrok 차선 | `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C1`, `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C2`, `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C4`, `raw/official-docs/ngrok-http-tunnel-official.md#NGROK-C1`, `raw/official-docs/ngrok-http-tunnel-official.md#NGROK-C3`, `raw/official-docs/ngrok-http-tunnel-official.md#NGROK-C4`, `raw/official-docs/google-oauth2-redirect-uri-validation-official.md#GOOGLE-REDIR-C3`, `raw/official-docs/google-oauth2-redirect-uri-validation-official.md#GOOGLE-REDIR-C5` | `official-vendor-doc + official-vendor-doc + official-vendor-doc` | Cloudflare Tunnel `.cfargotunnel.com` 도메인이 Google OAuth client redirect URI 정책 (`GOOGLE-REDIR-C2` raw IP 금지 / `C3` exact match) 을 통과하는지 직접 검증 안 됨 | ``` ```text $ sed -n '106p' 'raw/branch-notes/feature-keycloak-public-domain-tunneling.md' | D1 | 학습 환경 1순위 **Cloudflare Tunnel** (named tunnel + 정적 URL + 무료 TLS + inbound port 0) | 정적 public URL + inbound port 0 + 무료 TLS 가 필요하고 Cloudflare 에 위임한 관리 도메인 1개를 확보할 수 있을 때. 도메인이 없거나 1회성 데모면 → **D2**(ngrok) 또는 `trycloudflare.com` quick tunnel(random URL) | `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C1` (cloudflared outbound), `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C2` (firewall inbound 차단 권장), `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C3` (tunnel `.cfargotunnel.com` subdomain 자동 부여), `raw/official-docs/cloudflare-tunnel-routing-official.md#CLOUDFLARE-TUNNEL-C4` (사용자 hostname CNAME → cfargotunnel.com), `raw/official-docs/google-oauth2-redirect-uri-validation-official.md#GOOGLE-REDIR-C2` (Google redirect URI raw IP 금지 → 도메인 필요) | `official-vendor-doc + official-vendor-doc` | Cloudflare Tunnel 의 `.cfargotunnel.com` generic subdomain 이 Google Cloud Console redirect URI 정책에 통과하는지는 `CLOUDFLARE-TUNNEL-C4` "Does not prove" 단서에 명시된 미검증 사항 — P3B 실 검증 필요 | ``` This parent is also implicated in `L3-F08` and `L3-F16`. ## Grep verification - 실제 실행해 위에 출력한 `sed`/`grep` source lines V: 37 - byte-exact 일치 P: 37 - 라인 정정 C: 0 - 폐기 D: 0 ## Claim Traceability Check - Claim ID 연결 검사: lane-local decision references를 대조했고 `L3-F04`의 bare references 3건을 확인했다. 외부 raw Claim ID source truth 재검증은 slice 밖이다. - Decision Evidence Map: 10개 branch note의 DEM을 검토했다. 중복 또는 stale ownership은 `L3-F06`, `L3-F11`, `L3-F15`, `L3-F16`에 기록했다. - UNSUPPORTED_DECISION: 새 unsupported decision을 사실로 승격하지 않았다. 원문의 Max Reuse, family invalidation, proxy 관련 labels는 그대로 보존했다. ## Cross-lane candidates | Source edge | External owner / decision | Reason | | --- | --- | --- | | `feature-keycloak-nginx-auth-request-integration.md:50` | `feature-keycloak-edge-forwardauth-no-google D1` | standalone nginx 예시와 parent Kubernetes/Traefik split의 정합성 — `NEEDS_CROSS_LANE_AUDIT` | | `feature-keycloak-nginx-auth-request-integration.md:249` | `feature-keycloak-edge-forwardauth-no-google D2` | forwarded-header naming의 canonical owner — `NEEDS_CROSS_LANE_AUDIT` | | `feature-keycloak-nginx-auth-request-integration.md:253` | `feature-keycloak-header-spoofing-defense D5` | 요구하는 `X-Internal-Auth-Token`이 현재 nginx D4에 없음 — `NEEDS_CROSS_LANE_AUDIT` | | `feature-keycloak-oauth2-proxy-oidc-flow.md:194` | `feature-keycloak-docker-compose-stack D3` | discovery startup gating과 healthcheck 책임 — `NEEDS_CROSS_LANE_AUDIT` | | `feature-keycloak-realm-client-export.md:168` | `feature-keycloak-docker-compose-stack D4` | realm import mount/command ownership — `NEEDS_CROSS_LANE_AUDIT` | | `feature-keycloak-realm-client-export.md:182` | `feature-keycloak-single-ec2-no-google D3`, docker-compose D3 | hostname/redirect URI 전제와 bare sibling 귀속 — `NEEDS_CROSS_LANE_AUDIT` | | `feature-keycloak-reverse-proxy-headers.md:151` | `feature-keycloak-https-termination-caddy-nginx` | Caddy/nginx selection ownership — `NEEDS_CROSS_LANE_AUDIT` | | `feature-keycloak-reverse-proxy-headers.md:153`, `:258` | `feature-keycloak-header-spoofing-defense D6` | `KC_PROXY_TRUSTED_ADDRESSES` owner 정규화 — `NEEDS_CROSS_LANE_AUDIT` | | `feature-keycloak-reverse-proxy-headers.md:154` | `feature-keycloak-iss-claim-hostname-mismatch` | `KC_HOSTNAME` value owner와 parent D4의 중복 여부 — `NEEDS_CROSS_LANE_AUDIT` | | `feature-keycloak-single-ec2-google-federation.md:240`, `:279` | first-broker/account-linking siblings | parent D5와 consume-only 구현 절의 경계 — `NEEDS_CROSS_LANE_AUDIT` | | `feature-keycloak-single-ec2-google-federation.md:241`, `:301` | `feature-keycloak-google-redirect-uri-policy` | parent D6 exact URI와 delegated policy owner의 경계 — `NEEDS_CROSS_LANE_AUDIT` | ```wiki-stats agent: wiki-research-lane found: 10 processed: 10 dropped: 0 ```