## Lane Inventory | Path | Status | Evidence Lines | Extracted Facts | | --- | --- | --- | --- | | `raw/branch-notes/feature-keycloak-first-broker-login-flow.md` | READ_FULL | 1–261 | D1/D2 Confirm Link 소유증명, D4 silent auto-link 차단, hard-reject SPI 별도 트랙 | | `raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md` | READ_FULL | 1–238 | attribute-mapping owner, D3 IdP default IMPORT, D5 role mapping 위임 | | `raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md` | READ_FULL | 1–261 | exact redirect URI, basic-scope 예외, JavaScript origins 비움 | | `raw/branch-notes/feature-keycloak-header-spoofing-defense.md` | READ_FULL | 1–255 | K8s NetworkPolicy·EC2 SG·shared secret·trusted proxy 방어 경계 | | `raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md` | READ_FULL | 1–284 | TLS 종단 대안과 명시적 HSTS 구성 | | `raw/branch-notes/feature-keycloak-idp-brokering-google-client.md` | READ_FULL | 1–251 | Google IdP client, `trustEmail=false`, callback·origin 위임 | | `raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md` | READ_FULL | 1–148 | role/RBAC owner, D1 FORCE, D2 email verification 강제 주장 | | `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md` | READ_FULL | 1–512 | P2B composition hub, D8 zero-change brokering, D10 SMTP 사실, foreign-decision 상세 재진술 | | `raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md` | READ_FULL | 1–416 | P2A SPA Direct 정의, D2 refresh-token 저장, D4 brokering 비교 | | `raw/branch-notes/feature-keycloak-iss-claim-hostname-mismatch.md` | READ_FULL | 1–287 | issuer/JWKS 분리 대안, 미승인 F, 외부 owner 충돌 명시 | ## File 1 — `feature-keycloak-first-broker-login-flow.md` ### L2-F01: Confirm Link 보장의 버전 전제가 owner에 반영되지 않음 - **Severity:** High - **Failure modes:** `STALE_SUMMARY`, `UNSUPPORTED_DECISION` - **Source files:** first-broker-login-flow, idp-brokering-google-client, internal-spa-direct-google-federation - **Original goal:** - 인용: ```text core 차단(D1+D2)은 `trustEmail`·`email_verified` 검증과 무관하게 성립 ``` - 위치: `raw/branch-notes/feature-keycloak-first-broker-login-flow.md:135` - **Current state:** - 인용: ```text 이는 `trustEmail=false`+email 인증 상태에서도 Confirm Link 흐름이 우회 가능했던 실제(패치완료) 취약점 ``` - 위치: `raw/branch-notes/feature-keycloak-idp-brokering-google-client.md:214` - 인용: ```text `CVE-2026-9087` 은 **raw 에 아카이브된 출처가 없다** ``` - 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:338` - **Claim:** 문서 상태에 관한 FACT는 세 노트가 서로 다른 보장 강도를 제공한다는 것이다. 실제 취약점·영향 버전은 아직 `INFERENCE/UNVERIFIED`다. - **Assumptions:** F의 취약점 보고가 정확하고 배포 버전이 영향 범위일 때만 owner의 무조건부 보장이 실패한다. - **Concrete failure:** 구현자가 line 135만 소비하면 패치 상태를 확인하지 않고 Confirm Link를 충분한 차단으로 취급할 수 있다. - **Falsification:** 공식 Keycloak advisory가 해당 흐름에 영향이 없다고 밝히거나, 배포 버전이 영향 범위 밖임을 증명하면 보안 충돌은 사라진다. - **Required action / synthesis recommendation:** 공식 advisory를 raw로 보존한 뒤 owner D4에 버전 조건을 추가한다. 그 전에는 F의 “실제 취약점” 표현을 `needs-confirmation`으로 낮추고, owner에는 미검증 version gate만 둔다. - **Why:** 보안 owner의 보장은 소비자가 한 줄만 읽어도 유효 조건을 알 수 있어야 한다. - **Alternatives:** ① 패치 버전 allowlist 게이트, ② 취약 버전 배포 차단, ③ 별도 custom SPI 방어, ④ Google brokering 비활성화. - **Counterargument:** 현재 지원 버전만 사용하면 Confirm Link 보장은 유효할 수 있다. 그렇더라도 owner 문서에 적용 버전 조건을 적으면 이 반론과 충돌하지 않는다. - **Verification:** ```bash grep -nFo -- 'core 차단(D1+D2)은 `trustEmail`·`email_verified` 검증과 무관하게 성립' raw/branch-notes/feature-keycloak-first-broker-login-flow.md # Observed: 135:core 차단(D1+D2)은 `trustEmail`·`email_verified` 검증과 무관하게 성립 grep -nFo -- '이는 `trustEmail=false`+email 인증 상태에서도 Confirm Link 흐름이 우회 가능했던 실제(패치완료) 취약점' raw/branch-notes/feature-keycloak-idp-brokering-google-client.md # Observed: 214:이는 `trustEmail=false`+email 인증 상태에서도 Confirm Link 흐름이 우회 가능했던 실제(패치완료) 취약점 grep -nFo -- '`CVE-2026-9087` 은 **raw 에 아카이브된 출처가 없다**' raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md # Observed: 338:`CVE-2026-9087` 은 **raw 에 아카이브된 출처가 없다** ``` ### L2-F02: `email_verified=false` 정책이 silent-link 차단과 hard-reject 사이에서 갈림 - **Severity:** High - **Failure modes:** `CONTRADICTION`, `RESTATED_FOREIGN_DECISION` - **Source files:** first-broker-login-flow, idp-mappers-claim-to-role, internal-spa-direct-google-federation - **Original goal:** - 인용: ```text `email_verified=false` 계정의 **silent auto-link 차단** ``` - 위치: `raw/branch-notes/feature-keycloak-first-broker-login-flow.md:135` - 인용: ```text `email_verified=false` 를 flow 진입 즉시 *hard-reject* 하려면 커스텀 SPI authenticator 필요 → 별도 트랙 ``` - 위치: `raw/branch-notes/feature-keycloak-first-broker-login-flow.md:135` - **Current state:** - 인용: ```text `email_verified=true` 강제 (Attribute Importer + First Broker Login Flow custom step) ``` - 위치: `raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md:120` - 인용: ```text Google `email_verified=false` 계정 | 링크/생성 거부 ``` - 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:387` - **Claim:** owner D4는 소유증명 없는 auto-link만 차단한다. G와 H는 별도 SPI가 구현된 것처럼 전체 링크·생성을 거부한다고 요약한다. - **Assumptions:** slice 안에는 hard-reject custom authenticator의 별도 owner나 구현 증거가 없다. - **Concrete failure:** `email_verified=false` 사용자가 실제로는 Confirm Link 또는 신규 계정 흐름에 진입하는데, 테스트가 즉시 거부를 기대해 보안 정책과 구현이 어긋난다. - **Falsification:** 별도 owner가 custom authenticator 구현과 E2E 거부 결과를 제시하면 G/H의 정책이 성립한다. - **Required action / synthesis recommendation:** G/H를 owner D4의 “silent-link prevention only” 포인터로 축소한다. hard-reject가 필요하면 SPI branch를 단일 owner로 만들고 그 결정이 확정된 뒤 세 문서를 갱신한다. - **Why:** 계정 연결 방어와 모든 계정 생성 거부는 서로 다른 정책·구현 계약이다. - **Alternatives:** ① Confirm Link만 유지, ② custom SPI로 즉시 거부, ③ 통제된 enterprise IdP만 허용, ④ Google federation 자체를 사용하지 않음. - **Counterargument:** Google이 일반적으로 verified email을 제공하므로 차이가 드물 수 있다. 발생 빈도와 정책 보장은 별개이며, 문서는 미검증 claim의 실제 동작을 정의해야 한다. - **Verification:** ```bash grep -nFo -- '`email_verified=false` 계정의 **silent auto-link 차단**' raw/branch-notes/feature-keycloak-first-broker-login-flow.md # Observed: 135:`email_verified=false` 계정의 **silent auto-link 차단** grep -nFo -- '`email_verified=false` 를 flow 진입 즉시 *hard-reject* 하려면 커스텀 SPI authenticator 필요 → 별도 트랙' raw/branch-notes/feature-keycloak-first-broker-login-flow.md # Observed: 135:`email_verified=false` 를 flow 진입 즉시 *hard-reject* 하려면 커스텀 SPI authenticator 필요 → 별도 트랙 grep -nFo -- '`email_verified=true` 강제 (Attribute Importer + First Broker Login Flow custom step)' raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md # Observed: 120:`email_verified=true` 강제 (Attribute Importer + First Broker Login Flow custom step) grep -nFo -- 'Google `email_verified=false` 계정 | 링크/생성 거부' raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md # Observed: 387:Google `email_verified=false` 계정 | 링크/생성 거부 ``` ## File 2 — `feature-keycloak-google-claim-attribute-mapping.md` ### L2-F03: IMPORT owner와 FORCE 소비자의 적용 범위가 불명확함 - **Severity:** Medium - **Failure modes:** `AMBIGUOUS_OWNERSHIP`, 조건부 `CONTRADICTION` - **Source files:** google-claim-attribute-mapping, idp-mappers-claim-to-role - **Original goal:** - 인용: ```text 그쪽이 attribute-mapping **owner** ``` - 위치: `raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md:19` - **Current state:** - 인용: ```text | D1 | Sync Mode = `FORCE` ``` - 위치: `raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md:119` - 인용: ```text | D3 | Sync Mode = IMPORT (first login 시점 매핑만) ``` - 위치: `raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md:130` - 인용: ```text IdP-level `Sync Mode` = 모든 mapper default ``` - 위치: `raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md:130` - **Claim:** B가 IdP-level default를 IMPORT로 소유하지만, G의 D1은 FORCE가 global인지 role mapper override인지 밝히지 않는다. - **Assumptions:** G D1이 IdP-level 설정을 가리키면 두 값은 공존할 수 없다. mapper-level override이고 Keycloak이 해당 override를 지원한다면 공존 가능하다. - **Concrete failure:** 운영자가 동일 Admin UI 필드를 두 지침대로 번갈아 설정하거나, 모든 attribute를 로그인마다 덮어써 사용자 편집값을 잃을 수 있다. - **Falsification:** G D1을 특정 role mapper의 override로 명시하고 공식 근거·Admin UI 실측으로 override 우선순위를 확인하면 충돌이 해소된다. - **Required action / synthesis recommendation:** B D3를 IdP default owner로 유지하고, G D1은 role mapper 전용 override로 증명·범위화하거나 제거한다. - **Why:** fresh role과 보존해야 할 profile attribute는 서로 다른 갱신 정책을 가질 수 있으나 설정 계층을 분명히 해야 한다. - **Alternatives:** ① global IMPORT + role mapper FORCE, ② 전부 IMPORT, ③ 전부 FORCE, ④ role 계산을 별도 authorization 계층으로 이동. - **Counterargument:** role freshness에는 FORCE가 합리적이다. 이 finding은 FORCE 자체가 아니라 어느 설정 계층에 적용하는지 불명확한 점을 지적한다. - **Verification:** ```bash grep -nFo -- '그쪽이 attribute-mapping **owner**' raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md # Observed: 19:그쪽이 attribute-mapping **owner** grep -nFo -- '| D1 | Sync Mode = `FORCE`' raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md # Observed: 119:| D1 | Sync Mode = `FORCE` grep -nFo -- '| D3 | Sync Mode = IMPORT (first login 시점 매핑만)' raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md # Observed: 130:| D3 | Sync Mode = IMPORT (first login 시점 매핑만) grep -nFo -- 'IdP-level `Sync Mode` = 모든 mapper default' raw/branch-notes/feature-keycloak-google-claim-attribute-mapping.md # Observed: 130:| D3 | Sync Mode = IMPORT (first login 시점 매핑만) # Observed: 173:IdP-level `Sync Mode` = 모든 mapper default ``` ## File 3 — `feature-keycloak-google-redirect-uri-policy.md` ### L2-F04: basic-scope 예외를 옛 test-user 한도 설명이 덮음 - **Severity:** Low - **Failure mode:** `STALE_SUMMARY` - **Source file:** google-redirect-uri-policy - **Original goal:** - 인용: ```text 이 basic identity scope 조합은 test-user allowlist 등록·100명 상한·7일 만료·unverified 경고가 모두 면제됨 ``` - 위치: `raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md:143` - **Current state:** - 인용: ```text 학습용은 `openid email profile`만 사용 → unverified로 100명 test users까지 동작 ``` - 위치: `raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md:92` - **Claim:** D5의 정정 이후에도 TODO가 이전 한도 모델을 유지한다. - **Assumptions:** 요청 scope가 정말 `openid email profile`에 한정된다. - **Concrete failure:** 불필요한 test-user 등록·인원 관리 절차를 구현 계획에 넣고, 서로 다른 Google quota를 혼동한다. - **Falsification:** sensitive/restricted scope가 실제로 추가되면 TODO의 한도 설명이 별도 시나리오에서 다시 필요하다. - **Required action / synthesis recommendation:** line 92를 D5 포인터로 바꾸거나 삭제하고, sensitive-scope 시나리오는 별도 조건문으로 분리한다. - **Why:** 한 파일 안의 TODO가 authoritative D-row보다 오래된 운영 지침을 제공한다. - **Alternatives:** ① TODO 삭제 후 D5 포인터, ② 최신 예외 설명으로 갱신, ③ sensitive-scope 실험용 별도 TODO로 범위화. - **Counterargument:** Testing 상태의 다른 quota를 기억시키려는 문장일 수 있다. 그렇다면 basic identity scope와 다른 quota임을 명시해야 한다. - **Verification:** ```bash grep -nFo -- '이 basic identity scope 조합은 test-user allowlist 등록·100명 상한·7일 만료·unverified 경고가 모두 면제됨' raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md # Observed: 143:이 basic identity scope 조합은 test-user allowlist 등록·100명 상한·7일 만료·unverified 경고가 모두 면제됨 grep -nFo -- '학습용은 `openid email profile`만 사용 → unverified로 100명 test users까지 동작' raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md # Observed: 92:학습용은 `openid email profile`만 사용 → unverified로 100명 test users까지 동작 ``` ### L2-F05: JavaScript origins가 owner와 consumer에서 다름 - **Severity:** Medium - **Failure modes:** `STALE_SUMMARY`, `RESTATED_FOREIGN_DECISION` - **Source files:** google-redirect-uri-policy, idp-brokering-google-client - **Original goal:** - 인용: ```text | Authorized JavaScript origins | (비움) | ``` - 위치: `raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md:167` - **Current state:** - 인용: ```text Authorized JavaScript origins: `https://` ``` - 위치: `raw/branch-notes/feature-keycloak-idp-brokering-google-client.md:81` - **Claim:** redirect-policy owner는 server-side flow 때문에 origins를 비우지만, Google-client TODO는 외부 Keycloak host를 등록하라고 한다. - **Assumptions:** Google OAuth 교환은 Keycloak 서버가 수행하며 브라우저 Google SDK를 직접 사용하지 않는다. - **Concrete failure:** 불필요한 browser origin을 허용하고, 동일 client를 재현하는 사람마다 다른 Console 설정을 만든다. - **Falsification:** Google Console 실측에서 해당 client type이 origin을 요구하거나 실제 browser-side Google SDK 흐름이 추가되면 host 등록이 필요하다. - **Required action / synthesis recommendation:** F의 origin 값을 삭제하고 C D6 포인터로 교체한다. 브라우저 흐름이 생기면 별도 client와 owner를 둔다. - **Why:** callback URI와 JavaScript origin은 다른 정책 필드이며, consumer가 owner 값을 재정의하면 drift가 생긴다. - **Alternatives:** ① 비워 둠, ② browser flow 도입 시 최소 origin만 등록, ③ browser용 Google client 분리. - **Counterargument:** host 하나를 미리 등록해도 당장 실패하지 않을 수 있다. 그러나 사용하지 않는 권한 표면과 재현 차이는 남는다. - **Verification:** ```bash grep -nFo -- '| Authorized JavaScript origins | (비움) |' raw/branch-notes/feature-keycloak-google-redirect-uri-policy.md # Observed: 167:| Authorized JavaScript origins | (비움) | grep -nFo -- 'Authorized JavaScript origins: `https://`' raw/branch-notes/feature-keycloak-idp-brokering-google-client.md # Observed: 81:Authorized JavaScript origins: `https://` ``` ## File 4 — `feature-keycloak-header-spoofing-defense.md` **0 independent findings.** D3(K8s)과 D4(EC2)는 환경별 대안이며 동시에 적용해야 하는 상충 결정이 아니다. D5는 두 환경의 보조 계층, D6는 Keycloak proxy 신뢰 경계로 분리돼 있다. 다만 부모·proxy owner가 slice 밖이므로 lines 202–206은 `NEEDS_CROSS_LANE_AUDIT`로 이관한다. ## File 5 — `feature-keycloak-https-termination-caddy-nginx.md` ### L2-F06: Caddy HSTS 기본값 설명이 authoritative 구성과 상충함 - **Severity:** Medium - **Failure mode:** `STALE_SUMMARY` - **Source file:** https-termination-caddy-nginx - **Original goal:** - 인용: ```text **명시 필요** (default 로 단정 금지) ``` - 위치: `raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md:173` - **Current state:** - 인용: ```text Caddy는 디폴트로 HTTPS redirect + HSTS ``` - 위치: `raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md:43` - **Claim:** 구현 표는 HSTS를 명시하라고 정정했지만 초기 설계 답변은 여전히 자동 적용이라고 말한다. - **Assumptions:** 별도 global Caddy 설정이나 upstream edge가 HSTS를 주입하지 않는다. - **Concrete failure:** line 43을 따라 구성하면 HTTPS redirect는 동작해도 HSTS 헤더가 빠질 수 있다. - **Falsification:** 대상 Caddy 버전의 공식 문서나 응답 헤더 실측이 자동 HSTS를 입증하면 line 43이 맞다. - **Required action / synthesis recommendation:** line 43을 “automatic HTTPS redirect, HSTS explicit”으로 정정하고 D2/D5에만 값을 소유시킨다. - **Why:** TLS 자동화와 HSTS 정책은 서로 다른 기능이다. - **Alternatives:** ① Caddy `header` 명시, ② nginx에서 명시, ③ Cloudflare edge에서 단일 owner로 주입, ④ ephemeral dev에서는 의도적으로 생략하고 범위를 표시. - **Counterargument:** 학습 환경에서는 HSTS가 필요하지 않을 수 있다. 그렇다면 dev-only 생략 결정으로 적어야지 default 기능으로 설명하면 안 된다. - **Verification:** ```bash grep -nFo -- 'Caddy는 디폴트로 HTTPS redirect + HSTS' raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md # Observed: 43:Caddy는 디폴트로 HTTPS redirect + HSTS grep -nFo -- '**명시 필요** (default 로 단정 금지)' raw/branch-notes/feature-keycloak-https-termination-caddy-nginx.md # Observed: 173:**명시 필요** (default 로 단정 금지) ``` ## File 6 — `feature-keycloak-idp-brokering-google-client.md` **0 additional independent findings.** 이 파일은 L2-F01의 미아카이브 CVE 주장과 L2-F05의 stale JavaScript-origin TODO에 직접 참여한다. 그 외 D6의 `trustEmail=false` 값 선택과 default 미확정 구분은 내부적으로 일관된다. 부모가 정하는 ``는 slice 밖이므로 line 192를 교차 lane 후보로 남긴다. ## File 7 — `feature-keycloak-idp-mappers-claim-to-role.md` **0 additional independent findings.** 이 파일의 실제 상충은 L2-F02의 hard-reject 주장과 L2-F03의 FORCE 적용 범위로 모두 수거했다. role/RBAC만 소유한다는 line 19와 attribute-level D1/D2가 함께 남아 있는 것이 두 finding의 공통 원인이다. ## File 8 — `feature-keycloak-internal-spa-direct-google-federation.md` ### L2-F07: Reference-Only를 선언하면서 foreign decision 상세를 의도적으로 유지함 - **Severity:** Medium - **Failure mode:** `RESTATED_FOREIGN_DECISION` - **Source file:** internal-spa-direct-google-federation - **Original goal:** - 인용: ```text 본 노트는 `rules/consistency-contract.md` 의 **Reference-Only** 규약에 따라 포인터 + 1줄 요약으로만 인용한다 ``` - 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:22` - **Current state:** - 인용: ```text **유지 + 위임 고지** — 학습용 흐름 이해에 필요한 개요이므로 삭제하지 않고 ``` - 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:443` - **Claim:** 위임 고지는 Reference-Only의 “포인터 + 1줄” 제한을 대체하지 않는다. 파일 스스로 detailed restatement를 식별하면서 유지하기로 했다. - **Assumptions:** 현재 consistency-contract의 Single-Owner/Reference-Only 규칙이 그대로 적용된다. - **Concrete failure:** attribute mapping 또는 trust-chain owner가 바뀔 때 H의 상세 표가 같은 변경을 수동으로 따라가지 못해 stale copy가 된다. - **Falsification:** 규칙이 synchronized derived view를 허용하도록 바뀌고, owner 변경 때 자동 검증되는 생성물이 되면 상세 유지가 가능하다. - **Required action / synthesis recommendation:** branch-note에서는 owner 포인터와 한 줄 요약만 남긴다. 학습용 상세 설명은 canonical concept나 owner에서 생성되는 view로 이동한다. - **Why:** “owner가 우선”이라는 주석은 독자가 stale 상세를 실행하는 것을 막지 못한다. - **Alternatives:** ① 한 줄 pointer-only, ② canonical explainer로 이동, ③ owner 기반 자동 생성 view, ④ H가 새 owner가 되고 기존 owner를 pointer로 전환. - **Counterargument:** composition hub에는 end-to-end 이해를 위한 상세가 필요하다. 그 요구는 canonical explainer나 생성 view로 충족할 수 있다. - **Verification:** ```bash grep -nFo -- '본 노트는 `rules/consistency-contract.md` 의 **Reference-Only** 규약에 따라 포인터 + 1줄 요약으로만 인용한다' raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md # Observed: 22:본 노트는 `rules/consistency-contract.md` 의 **Reference-Only** 규약에 따라 포인터 + 1줄 요약으로만 인용한다 grep -nFo -- '**유지 + 위임 고지** — 학습용 흐름 이해에 필요한 개요이므로 삭제하지 않고' raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md # Observed: 443:**유지 + 위임 고지** — 학습용 흐름 이해에 필요한 개요이므로 삭제하지 않고 ``` ### L2-F08: brokering zero-change payload에 두 owner가 존재함 - **Severity:** Medium - **Failure mode:** `DUAL_OWNERSHIP` - **Source files:** internal-spa-direct-google-federation, internal-spa-direct-no-google - **Original goal:** - 인용: ```text P2B 의 federation 은 **Keycloak 레이어에서만** broker ``` - 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:324` - **Current state:** - 인용: ```text brokering 은 SPA/Backend **코드 0줄**로 얹히는 cross-cutting ``` - 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md:282` - 인용: ```text **이중 주장 (`/sync` 수거 대상)** ``` - 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:402` - **Claim:** H D8과 I D4가 동일한 zero-change 결론을 각각 결정 payload로 보유하며 H도 이를 이중 주장으로 인정한다. - **Assumptions:** “Keycloak-only broker”와 “SPA/backend code zero”가 같은 구조적 보장의 두 표현이다. - **Concrete failure:** `kc_idp_hint`, 로그인 버튼 노출 기본값, SPA 변경 조건이 진화할 때 두 D-row가 다른 경계를 가질 수 있다. - **Falsification:** I D4를 범위 선택과 H D8 pointer만 남기도록 축소하면 소유권이 분리된다. - **Required action / synthesis recommendation:** H D8을 composition owner로 지정하고 I D4를 포인터로 전환한다. 별도 `federation-spa-zero-change` owner를 택하려면 먼저 그 unseen branch와 교차 감사를 수행한다. - **Why:** 아키텍처 불변성은 여러 구현 결정의 전제가 되므로 한 곳에서 조건과 예외를 소유해야 한다. - **Alternatives:** ① H D8 owner, ② I D4 owner, ③ zero-change 전용 child owner, ④ project hub의 고정 결정으로 승격. - **Counterargument:** baseline과 extension 양쪽에 같은 설명이 있어야 읽기 쉽다. pointer와 한 줄 요약이면 가독성을 유지하면서 owner를 하나로 둘 수 있다. - **Verification:** ```bash grep -nFo -- 'P2B 의 federation 은 **Keycloak 레이어에서만** broker' raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md # Observed: 324:P2B 의 federation 은 **Keycloak 레이어에서만** broker grep -nFo -- 'brokering 은 SPA/Backend **코드 0줄**로 얹히는 cross-cutting' raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md # Observed: 282:brokering 은 SPA/Backend **코드 0줄**로 얹히는 cross-cutting grep -nFo -- '**이중 주장 (`/sync` 수거 대상)**' raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md # Observed: 402:**이중 주장 (`/sync` 수거 대상)** ``` L2-F01과 L2-F02도 이 파일의 version gate 및 `email_verified` edge table에 직접 적용된다. D10의 SMTP 소유권은 실제 docker-compose owner가 slice 밖이라 finding으로 확정하지 않고 교차 lane 후보로 남겼다. ## File 9 — `feature-keycloak-internal-spa-direct-no-google.md` ### L2-F09: SPA Direct 정의 안에 BFF refresh-cookie 선택을 넣음 - **Severity:** High - **Failure modes:** `INCOMPATIBLE_ASSUMPTIONS`, `RESTATED_FOREIGN_DECISION` - **Source file:** internal-spa-direct-no-google - **Original goal:** - 인용: ```text **Edge proxy 없음.** ``` - 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md:17` - **Current state:** - 인용: ```text **secure httpOnly cookie** (BFF 변형) ``` - 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md:188` - 인용: ```text | D2 | refresh token 저장 위치 = secure httpOnly cookie 권장 ``` - 위치: `raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md:280` - **Claim:** `INFERENCE` — HttpOnly refresh cookie를 발급·회전·소비하려면 token-mediating server가 필요하다. 정의된 backend는 Resource Server일 뿐이고 edge도 없으므로 D2 권장은 현재 패턴 안에서 실행할 주체가 없다. - **Assumptions:** 브라우저 SPA는 HttpOnly cookie 값을 읽을 수 없고, Keycloak이 이 프로젝트 origin용 refresh cookie를 대신 관리하도록 설계되지 않았다. - **Concrete failure:** 구현자가 Resource Server에 token exchange·cookie issuance·CSRF 방어를 뒤늦게 추가하면서 P2A를 사실상 BFF/AP3로 바꾸지만 문서·threat model은 SPA Direct로 남는다. - **Falsification:** 별도 mediator 없이 Keycloak/browser adapter가 usable HttpOnly refresh cookie를 제공한다는 공식 근거와 E2E 결과가 있거나, 이미 mediator가 아키텍처에 포함돼 있음을 증명하면 성립하지 않는다. - **Required action / synthesis recommendation:** P2A 기본은 memory-only로 두거나, HttpOnly 선택 시 명시적으로 token-mediating/BFF 패턴으로 전환하고 CSRF·rotation owner를 함께 연결한다. unseen token-storage owner도 교차 감사해야 한다. - **Why:** storage 선택이 토큰 보유 주체와 인증 아키텍처를 바꾸고 있다. - **Alternatives:** ① memory-only, ② Web Worker 격리, ③ 최소 token-mediator 추가, ④ full BFF, ⑤ browser storage 사용 후 XSS 위험 수용. - **Counterargument:** reload 후 세션 유지에는 HttpOnly cookie UX가 낫다. 그 장점은 mediator 도입 비용과 CSRF 계약을 명시적으로 수용할 때만 비교할 수 있다. - **Verification:** ```bash grep -nFo -- '**Edge proxy 없음.**' raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md # Observed: 17:**Edge proxy 없음.** grep -nFo -- '**secure httpOnly cookie** (BFF 변형)' raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md # Observed: 188:**secure httpOnly cookie** (BFF 변형) grep -nFo -- '| D2 | refresh token 저장 위치 = secure httpOnly cookie 권장' raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md # Observed: 280:| D2 | refresh token 저장 위치 = secure httpOnly cookie 권장 ``` L2-F08의 zero-change 이중 소유도 이 파일 D4에 적용된다. ## File 10 — `feature-keycloak-iss-claim-hostname-mismatch.md` **0 slice-internal findings.** D6의 `jwk-set-uri` 대안은 부모와 audience-validator owner가 승인하기 전까지 권고안으로만 유지되고, 현재 기본을 C로 명시해 파일 내부에서 두 구성을 동시에 채택하지 않는다. `KC-HOST-C6`는 미아카이브 claim 후보라고 lines 142·230·247에 명시돼 Supporting Claim으로 위장하지 않는다. 외부 owner 충돌은 lines 204–209의 교차 lane 후보로 분리한다. ## Synthesis 이 slice에는 High 3건, Medium 5건, Low 1건의 정합 gap이 있다. 가장 직접적인 보안 오해는 `email_verified=false`를 silent-link 차단과 hard-reject로 혼용한 L2-F02다. L2-F01의 버전 위험은 파급은 크지만 공식 advisory가 raw에 없어 실재·영향 범위를 아직 FACT로 취급할 수 없다. IMPORT/FORCE, JavaScript origins, HSTS는 owner 값보다 오래된 소비자·초기 설명이 남은 drift다. P2B는 Reference-Only를 선언하면서 상세 foreign decisions를 유지하고, zero-change 결론도 P2A와 공동 소유한다. P2A의 HttpOnly-cookie 권장은 SPA Direct와 BFF의 경계를 흐리므로 구현 전 owner 결정이 필요하다. Header-spoofing과 issuer-mismatch 파일은 slice 내부에서는 대안 조건과 미승인 상태를 구분하고 있다. ## Inferences 1. CVE 보고가 정확할 때만 owner의 무조건부 Confirm Link 보장이 영향 버전에서 실패한다 — Based on: `feature-keycloak-idp-brokering-google-client.md:214`, `feature-keycloak-internal-spa-direct-google-federation.md:338`. 2. G D1이 mapper-level override라면 IMPORT/FORCE는 공존할 수 있다 — Based on: `feature-keycloak-google-claim-attribute-mapping.md:130`, `feature-keycloak-idp-mappers-claim-to-role.md:119`. 3. HttpOnly refresh cookie에는 token-mediating server가 필요하다 — Based on: `feature-keycloak-internal-spa-direct-no-google.md:17,188,280`. 실제 adapter 동작으로 반증 가능하다. ## Out of Scope - Slice 밖 parent/project/owner branch의 본문 의미 감사 - Keycloak·Google Console·Caddy 실제 구동 - CVE 공식 advisory 외부 fetch 및 raw 보존 - branch-note 수정 또는 canonical 문서 생성 ## Coverage Gaps - Files marked NOT_READ or BLOCKED: none - Whole-corpus conclusions: INCOMPLETE — 아래 unseen owner/decision을 교차 lane에서 확인해야 한다. ## Grep Verification 각 finding 안에 25개 quote pattern의 절대경로 명령과 observed output을 기록했다. 대표 확인: ```bash grep -nFo -- 'core 차단(D1+D2)은 `trustEmail`·`email_verified` 검증과 무관하게 성립' raw/branch-notes/feature-keycloak-first-broker-login-flow.md # Observed: 135:core 차단(D1+D2)은 `trustEmail`·`email_verified` 검증과 무관하게 성립 grep -nFo -- '| D2 | refresh token 저장 위치 = secure httpOnly cookie 권장' raw/branch-notes/feature-keycloak-internal-spa-direct-no-google.md # Observed: 280:| D2 | refresh token 저장 위치 = secure httpOnly cookie 권장 ``` - 검증한 verbatim quote 총 개수 V: 25 - 일치 P: 25 - 라인 정정 C: 0 - 폐기 D: 0 ## Claim Traceability Check - Claim ID 연결 검사: slice에서 추출한 unique Claim ID 153개 중 152개가 branch/project note 밖의 raw source에서 lexical resolve. `KC-HOST-C6` 1개는 iss-claim-hostname-mismatch:142·230·247이 명시한 미아카이브 후보이며 Supporting Claim으로 사용되지 않아 `BROKEN_CLAIM_REFERENCE`는 none found. - Decision Evidence Map: 10/10 파일에서 1개씩, 총 10개 검토. 이번 lane은 raw Claim ID 존재성까지 확인했으며 각 raw claim의 의미 재감사는 범위 밖이다. - UNSUPPORTED_DECISION: 정합 finding과 직접 연결된 3개 축 — CVE version gate(H:338), JavaScript origins 비움(C:73·167), P2A brokering scope/zero-change(I:282). 그 밖의 project-scope·implementation caveat는 파일이 명시적으로 라벨링해 상충 finding으로 승격하지 않았다. ## Cross-Lane Audit Candidates | File:line | Referenced owner / decision | Reason | | --- | --- | --- | | `feature-keycloak-google-redirect-uri-policy.md:202-205` | single-EC2-google-federation D4/D6; public-domain-tunneling D1/D2 | callback host/path와 tunnel 선택 owner가 unseen | | `feature-keycloak-header-spoofing-defense.md:202-206` | edge-forwardauth-no-google D2/D4; nginx-auth-request; reverse-proxy-headers | 부모 enforcement 위임과 신규 `X-Internal-Auth-Token` 계약 확인 필요 | | `feature-keycloak-https-termination-caddy-nginx.md:232-233` | reverse-proxy-headers D1; public-domain-tunneling | TLS 종단 위치와 forwarded-header mode 결합 검증 필요 | | `feature-keycloak-idp-brokering-google-client.md:189,192` | account-linking-sub-vs-email; edge-forwardauth-google-federation | `trustEmail` 소비 의미와 `` owner 확인 필요 | | `feature-keycloak-internal-spa-direct-google-federation.md:403` | 미지정 CSRF/`state` owner | 파일이 project-wide owner 부재를 주장하므로 다른 lane 확인 필요 | | `feature-keycloak-internal-spa-direct-google-federation.md:404` | docker-compose-stack / realm configuration | H D10의 SMTP deployment-fact 소유권이 실제 stack owner와 겹치는지 확인 필요 | | `feature-keycloak-internal-spa-direct-google-federation.md:407-408` | account-linking-sub-vs-email D1; three-leg-trust-chain D1/D2/D5 | L2-F07의 foreign-detail summary가 실제 owner와 같은지 확인 필요 | | `feature-keycloak-internal-spa-direct-no-google.md:365-368` | audience-validator; token-storage; refresh-rotation; PKCE-flow | Hubs의 foreign D-row 요약과 L2-F09 token-storage owner 확인 필요 | | `feature-keycloak-iss-claim-hostname-mismatch.md:204-209` | single-EC2-no-google D3; audience-validator D6; docker-compose D3/D5 | 파일이 이미 owner conflict·trigger erosion·3자 재진술을 명시함 | ## Stats ```wiki-stats agent: wiki-research-lane found: 10 processed: 10 dropped: 0 ```