Files
llm-wiki/docs/superpowers/specs/2026-07-18-keycloak-branch-note-consistency/lanes/lane-04-keycloak-sorted-31-38.md
T

44 KiB
Raw Blame History

Lane Inventory

Path Status Evidence Lines Extracted Facts
raw/branch-notes/feature-keycloak-single-ec2-no-google.md READ_FULL 1342 3 findings
raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md READ_FULL 1256 3 findings
raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md READ_FULL 1268 2 findings
raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md READ_FULL 1231 2 findings
raw/branch-notes/feature-keycloak-three-leg-trust-chain.md READ_FULL 1229 3 findings
raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md READ_FULL 1237 2 findings
raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md READ_FULL 1257 3 findings
raw/branch-notes/feature-security-operational-baseline.md READ_FULL 1417 3 findings

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

L4-F01 — localhost issuer와 extra_hosts 해결책이 실행 가능한 한 가지 구성으로 종결되지 않음

  • Severity / 유형: High — ambiguous/incompatible default flow
  • Source quotes:
    • "extra_hosts: [host.docker.internal:host-gateway]"raw/branch-notes/feature-keycloak-single-ec2-no-google.md:146
    • "backend도 \issuer-uri=http://localhost:8080/...` 사용. 컨테이너 간 통신은 `network_mode: host` 또는 `extra_hosts` 사용."raw/branch-notes/feature-keycloak-single-ec2-no-google.md:181`
  • Claim: bridge-network backend 컨테이너에서 localhost는 backend 자신이다. extra_hostshost.docker.internal 이름을 추가할 뿐 localhost를 호스트로 바꾸지 않는다. 두 메커니즘을 동일한 해결책처럼 병기하면 기본 구성이 실행 불가능하다.
  • Assumptions: backend도 Docker bridge network에서 실행되고, 별도 jwk-set-uri override가 없다.
  • Failure mode: OIDC discovery/JWKS 접근 실패로 backend startup 실패 또는 전 요청 401.
  • Falsification: 실제 기본이 network_mode: host, backend host-process 실행, 또는 issuer-uri=localhostjwk-set-uri=host.docker.internal을 명시적으로 분리한 구성이라면 비판이 무효화된다.
  • Required action / Why: feature-keycloak-iss-claim-hostname-mismatch owner에서 기본 한 가지를 고정하고 이 노트는 포인터만 유지한다. issuer 문자열 일치와 네트워크 도달성은 서로 다른 조건이기 때문이다.
  • Alternatives:
    1. host network + issuer-uri=localhost
    2. bridge + issuer-uri=localhost + 명시적 jwk-set-uri=host.docker.internal
    3. front/backchannel 동적 URL을 명시적으로 구성
  • Counterargument: extra_hosts로 호스트 접근은 가능하다. 다만 접근 hostname은 host.docker.internal이므로, 현재 적힌 localhost discovery URI와 자동으로 결합되지는 않는다.
  • Self-grep:
$ grep -nFo -e 'backend도 `issuer-uri=http://localhost:8080/...` 사용. 컨테이너 간 통신은 `network_mode: host` 또는 `extra_hosts` 사용.' -e 'extra_hosts: [host.docker.internal:host-gateway]' -- raw/branch-notes/feature-keycloak-single-ec2-no-google.md
146:extra_hosts: [host.docker.internal:host-gateway]
181:backend도 `issuer-uri=http://localhost:8080/...` 사용. 컨테이너 간 통신은 `network_mode: host` 또는 `extra_hosts` 사용.

L4-F02 — aud validator owner 정정 후 컴포넌트 표가 옛 owner를 계속 지목

  • Severity / 유형: Medium — STALE_SUMMARY
  • Source quotes:
  • Claim: 같은 문서가 owner를 audience-validator로 정정한 뒤 실행 표에서는 여전히 deferred RBAC 문서의 D2를 가리킨다.
  • Assumptions: line 110의 2026-07-18 owner split이 최신 결정이다.
  • Failure mode: 구현자가 role-mapping의 복제된 validator를 수정하고 정본과 다시 갈라진다.
  • Falsification: role-mapping D2가 여전히 공동 owner라면 Single-Owner 선언 자체를 철회해야 한다.
  • Required action / Why: line 215와 동일 요약들을 [[...spring-rs-audience-validator]] D1로 바꾸고 role-mapping은 RBAC D4D6만 가리킨다.
  • Alternatives: RS/audience 전체를 role-mapping으로 다시 이관할 수도 있으나 두 문서 중 하나만 owner여야 한다.
  • Counterargument: 표가 역사적 child 목록일 수 있다. 그러나 셀 문구가 현재 구현 근거를 직접 지목하므로 역사 기록으로 읽히지 않는다.
  • Self-grep:
$ grep -nFo -e 'Spring RS 셋업·`aud` 검증 = [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 가 owner' -e '`aud` validator 는 child [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] D2' -- raw/branch-notes/feature-keycloak-single-ec2-no-google.md
110:Spring RS 셋업·`aud` 검증 = [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 가 owner
215:`aud` validator 는 child [[raw/branch-notes/feature-keycloak-spring-rs-role-mapping]] D2

L4-F03 — PKCE 구현 순서가 parent와 실행 owner에서 반대

  • Severity / 유형: Medium — CONTRADICTION
  • Source quotes:
    • "vanilla JS는 manual fetch + \crypto.subtle` 기반 PKCE 구현 우선"...single-ec2-no-google.md:182`
    • "\oidc-client-ts` 우선, manual은 별도 단계."raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md:106`
  • Claim: parent는 manual-first, 실행 owner는 library-first를 결정한다.
  • Assumptions: 두 문구 모두 실제 코딩 순서를 뜻한다.
  • Failure mode: 작업 순서·완료 조건·테스트 baseline이 달라진다.
  • Falsification: 전자를 사전 학습 순서, 후자를 repo 구현 순서로 명시하면 양립 가능하다.
  • Required action / Why: 실행 순서는 child D1을 정본으로 두고 parent D4를 “manual 사전 학습 선택지”로 재분류한다.
  • Alternatives: manual spike → 폐기 → library 구현, 또는 library E2E → manual 비교 실습.
  • Counterargument: 결국 둘 다 수행하므로 충돌이 작다. 하지만 단계별 구현 branch의 착수 순서와 acceptance criteria에는 직접 영향을 준다.
  • Self-grep:
$ grep -nFo -e 'vanilla JS는 manual fetch + `crypto.subtle` 기반 PKCE 구현 우선' -- raw/branch-notes/feature-keycloak-single-ec2-no-google.md
182:vanilla JS는 manual fetch + `crypto.subtle` 기반 PKCE 구현 우선

$ grep -nFo -e '`oidc-client-ts` 우선, manual은 별도 단계.' -- raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md
106:`oidc-client-ts` 우선, manual은 별도 단계.

raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md

L4-F04 — “SPA Direct, BFF 미채택” 기본이 실제로는 TMB를 요구

  • Severity / 유형: High — incompatible architecture default
  • Source quotes:
    • "P2A 권장 조합 = access_token: 메모리 + refresh_token: secure httpOnly cookie"...spa-token-storage-tradeoff.md:140
    • "사실상 최소 Token-Mediating Backend(TMB). \"순수 no-server SPA Direct\" 와의 긴장"...spa-token-storage-tradeoff.md:165
  • Claim: 브라우저 JS는 HttpOnly cookie를 설정할 수 없다. D1을 구현하려면 backend가 refresh token을 받아 Set-Cookie하고 /refresh를 소유해야 하므로 순수 SPA Direct가 아니다.
  • Assumptions: “SPA Direct”가 token endpoint를 브라우저가 직접 호출하고 server-side token custody가 없다는 뜻이다.
  • Failure mode: HttpOnly 저장이 구현되지 않거나, 암묵적으로 TMB를 추가하면서 CSRF·rotation·logout 계약이 빠진다.
  • Falsification: 프로젝트 taxonomy가 Resource Server의 /refresh 추가를 여전히 AP1로 명시적으로 허용하면 무효화된다.
  • Required action / Why: D1을 순수 AP1과 TMB 변형으로 분리하고 기본을 하나 고정한다. token custody가 인증 패턴을 바꾸기 때문이다.
  • Alternatives:
    1. 순수 SPA: 양 토큰 memory-only + reload 재로그인
    2. TMB: refresh token HttpOnly cookie + /refresh
    3. BFF: 모든 token server-side
  • Counterargument: 기존 Resource Server에 endpoint 하나만 추가하면 된다. 그래도 refresh token 보유·CSRF·cookie lifecycle 책임이 추가되어 trust boundary가 달라진다.
  • Self-grep:
$ grep -nFo -e 'P2A 권장 조합 = access_token: 메모리 + refresh_token: secure httpOnly cookie' -e '사실상 최소 Token-Mediating Backend(TMB). "순수 no-server SPA Direct" 와의 긴장' -- raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md
140:P2A 권장 조합 = access_token: 메모리 + refresh_token: secure httpOnly cookie
165:사실상 최소 Token-Mediating Backend(TMB). "순수 no-server SPA Direct" 와의 긴장

L4-F05 — D3·D6 근거 상태를 해소한 뒤 Claims To Verify가 계속 UNSUPPORTED로 기록

  • Severity / 유형: Medium — STALE_SUMMARY
  • Source quotes:
    • "D3 를 브라우저·토폴로지 조건부로 **정밀화**"...spa-token-storage-tradeoff.md:131
    • "D6 를 \UNSUPPORTED` 에서 해소"...spa-token-storage-tradeoff.md:132`
    • "본 branch D3 의 근거가 UNSUPPORTED"...spa-token-storage-tradeoff.md:214
    • "D6 가 UNSUPPORTED"...spa-token-storage-tradeoff.md:216
  • Claim: active decision과 검증 큐가 같은 근거 상태를 다르게 표시한다.
  • Assumptions: 2026-07-18 decision update가 최신이다.
  • Failure mode: 이미 조사한 근거를 재조사하거나, 실제로 남은 runtime 검증과 문헌 근거 갭을 혼동한다.
  • Falsification: Claims 행이 과거 상태를 보존하는 역사 섹션이면 날짜와 resolved 표기가 있어야 한다.
  • Required action / Why: D3는 “근거 확보, topology별 E2E planned”, D6는 “inference 근거 확보, 저장 동작 E2E planned”로 분리한다.
  • Alternatives: 해소된 행을 resolved archive로 이동하고 새 empirical claim만 남긴다.
  • Counterargument: 구현 검증은 아직 안 됐다. 그렇더라도 “문헌 unsupported”와 “runtime unverified”는 다른 상태다.
  • Self-grep:
$ grep -nFo -e 'D3 를 브라우저·토폴로지 조건부로 **정밀화**' -e '본 branch D3 의 근거가 UNSUPPORTED' -e 'D6 를 `UNSUPPORTED` 에서 해소' -e 'D6 가 UNSUPPORTED' -- raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md
131:D3 를 브라우저·토폴로지 조건부로 **정밀화**
132:D6 를 `UNSUPPORTED` 에서 해소
214:본 branch D3 의 근거가 UNSUPPORTED
216:D6 가 UNSUPPORTED
  • Severity / 유형: Medium — ambiguous owner / owner-less concern
  • Source quotes:
  • Claim: storage 문서는 CSRF 실 구현을 audience-validator “계열”로 보냈지만, 대상 문서는 bearer JWT 검증만 소유하며 CSRF D-row가 없다. 포인터도 D<n> 없이 광범위하다.
  • Assumptions: HttpOnly refresh cookie/TMB 변형을 실제 선택한다.
  • Failure mode: bearer API의 csrf.disable()를 cookie refresh endpoint에도 적용하거나, CSRF token 발급·검증을 아무 branch도 구현하지 않는다.
  • Falsification: 별도 unseen owner가 CSRF endpoint 계약을 이미 소유한다면 그 owner D-row를 직접 가리키면 된다.
  • Required action / Why: TMB/BFF refresh endpoint owner에 CSRF Decision을 두고 여기서는 정확한 D 포인터만 유지한다.
  • Alternatives: audience-validator가 범위를 확장할 경우 명시적 D-row·cookie endpoint scope·음성 테스트를 추가한다.
  • Counterargument: 같은 SecurityFilterChain에서 구현할 수 있다. 구현 위치와 결정 owner는 별개이며 bearer와 cookie 요청은 CSRF 위협 모델이 다르다.
  • Self-grep:
$ grep -nFo -e 'Spring Security `CsrfTokenRepository` 설정/검증은 [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 계열 RS 결정으로 위임' -- raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md
63:Spring Security `CsrfTokenRepository` 설정/검증은 [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 계열 RS 결정으로 위임

$ grep -nFo -e '본 노트는 발급된 토큰의 **검증 측**만.' -- raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md
64:본 노트는 발급된 토큰의 **검증 측**만.
223:본 노트는 발급된 토큰의 **검증 측**만.

raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md

L4-F07 — “custom validator 필수” 요약이 최신 property-first 선택 조건과 어긋남

  • Severity / 유형: Medium — STALE_SUMMARY
  • Source quotes:
    • "custom \OAuth2TokenValidator`를 추가해야 하는 이유"...spring-rs-audience-validator.md:17`
    • "단일 audience** 면 property 한 줄이 단순·안전(권장)"...spring-rs-audience-validator.md:183
  • Claim: 목적·본문은 custom validator를 필수로 표현하지만 최신 구현 가이드는 단일 audience에서 Boot audiences property를 권장 baseline으로 둔다.
  • Assumptions: 본 프로젝트는 우선 단일 expected audience다.
  • Failure mode: 불필요한 custom decoder wiring에서 default issuer/timestamp validator를 덮어쓰거나 누락한다.
  • Falsification: 다중 audience/azp 조건부 검증이 고정 요구라면 custom이 필수일 수 있다.
  • Required action / Why: “aud 검증은 필수, 구현 방식은 단일=property / 복합=custom”으로 목표·TODO·면접 문장을 동기화한다.
  • Alternatives: custom 방식은 비교 학습 예제로 남긴다.
  • Counterargument: custom 구현 학습이 목적이다. 학습 예제라는 점과 production 기본을 분리하면 된다.
  • Self-grep:
$ grep -nFo -e 'custom `OAuth2TokenValidator<Jwt>`를 추가해야 하는 이유' -e '단일 audience** 면 property 한 줄이 단순·안전(권장)' -- raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md
17:custom `OAuth2TokenValidator<Jwt>`를 추가해야 하는 이유
183:단일 audience** 면 property 한 줄이 단순·안전(권장)

L4-F08 — expected audience가 backend-client-idspa-client로 갈림

  • Severity / 유형: High — CONTRADICTION
  • Source quotes:
    • "backend client_id 로 \aud` 를 검증"...spring-rs-audience-validator.md:156`
    • "\aud=spa-client`를 강제했습니다"...spring-rs-role-mapping.md:38`
  • Claim: 정본은 API/backend용 audience를 요구하지만 소비 문서는 SPA public client ID를 expected audience로 고정한다.
  • Assumptions: backend-client-idspa-client는 별도 client다.
  • Failure mode: 정상 토큰이 모두 401이 되거나, 잘못된 resource audience를 허용해 cross-resource token reuse 방어가 무효화된다.
  • Falsification: API audience 식별자를 의도적으로 spa-client와 동일하게 정의했다면 두 문구는 같은 값을 뜻할 수 있다. 이 경우 owner가 그 동일성부터 명시해야 한다.
  • Required action / Why: realm/client-export owner와 함께 expected audience 심볼 하나를 고정하고 mapper·validator·음성 테스트에 동일하게 사용한다.
  • Alternatives: backend client ID, API audience URI, 또는 의도적으로 SPA client ID를 API audience로 재사용.
  • Counterargument: Keycloak public client가 access token audience일 수 있다. 그렇다면 “backend client_id mapper 필수” 결정이 잘못된 것이므로 둘 중 하나를 고쳐야 한다.
  • Self-grep:
$ grep -nFo -e 'backend client_id 로 `aud` 를 검증' -- raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md
156:backend client_id 로 `aud` 를 검증

$ grep -nFo -e '`aud=spa-client`를 강제했습니다' -- raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md
38:`aud=spa-client`를 강제했습니다

raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md

L4-F09 — RS/audience 위임 선언 뒤 active scope·TODO에서 세부를 계속 재명세

  • Severity / 유형: Medium — RESTATED_FOREIGN_DECISION
  • Source quotes:
    • "Spring RS 공통 셋업 + \aud` 검증 내용은 형제 raw/branch-notes/feature-keycloak-spring-rs-audience-validatorowner"...spring-rs-role-mapping.md:19`
    • "custom \OAuth2TokenValidator` for `aud=spa-client`"...spring-rs-role-mapping.md:50`
    • "여기서 재명세하지 않고"...spring-rs-role-mapping.md:128
  • Claim: owner 정리는 정확하지만 제목·WHY·In scope·TODO가 custom validator, issuer, decoder를 실행 항목으로 계속 보유한다.
  • Assumptions: line 19/128의 owner 이관이 최신이다.
  • Failure mode: audience 구현이 두 branch에서 독립 진화하며 audience 값과 wiring이 갈라진다.
  • Falsification: 이 파일이 역사적 migration snapshot이라면 active scope/TODO가 아니라 Audit 섹션에 있어야 한다.
  • Required action / Why: 파일을 RBAC-only로 축소하고 RS-common은 [[...audience-validator]] D1/D6 — 1줄 요약만 남긴다.
  • Alternatives: 반대로 전체 RS를 이 파일로 합치고 audience-validator를 폐기할 수 있으나 Single-Owner는 하나여야 한다.
  • Counterargument: 구현 편의를 위한 fold-in이다. 세부 복제는 owner 진화 시 현재처럼 aud 값이 갈라지는 원인이 된다.
  • Self-grep:
$ grep -nFo -e 'Spring RS 공통 셋업 + `aud` 검증 내용은 형제 [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 가 **owner**' -e 'custom `OAuth2TokenValidator<Jwt>` for `aud=spa-client`' -e '여기서 재명세하지 않고' -- raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md
19:Spring RS 공통 셋업 + `aud` 검증 내용은 형제 [[raw/branch-notes/feature-keycloak-spring-rs-audience-validator]] 가 **owner**
50:custom `OAuth2TokenValidator<Jwt>` for `aud=spa-client`
128:여기서 재명세하지 않고

L4-F10 — realm-role-only 결정의 “단일 client” 전제가 4-pattern/client base와 stale

  • Severity / 유형: Medium — assumption drift / STALE_SUMMARY
  • Source quotes:
    • "단일 client 환경(client-level role 분리 의미 적음)"...spring-rs-role-mapping.md:122
    • "4 패턴 통합 base 로 확장 시 realm 은 공유 1개 유지, client 는 패턴당 1개"...single-ec2-no-google.md:199
  • Claim: RBAC D4의 선택 조건은 client가 하나라는 전제인데 현재 배포 base는 패턴별 여러 client를 둔다.
  • Assumptions: deferred authZ를 4개 패턴 통합 base에 적용할 예정이다.
  • Failure mode: 패턴별 권한 분리가 필요한데 realm role이 모든 client에 전역 적용되거나, 반대로 필요한 공통 role이 중복 정의된다.
  • Falsification: RBAC가 모든 client에 의도적으로 동일한 realm-global 정책이면 유지 가능하다.
  • Required action / Why: D4 선택 조건을 “realm-global 권한” 대 “client-specific 권한”으로 재작성하고 client 수를 근거로 삼지 않는다.
  • Alternatives: realm-only, resource_access only, 공통 realm role + 민감 endpoint client role 병행.
  • Counterargument: authZ는 deferred라 지금 결정할 필요가 없다. 그렇다면 “단일 client라 의미 없음”이라는 확정 이유도 deferred로 낮춰야 한다.
  • Self-grep:
$ grep -nFo -e '단일 client 환경(client-level role 분리 의미 적음)' -- raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md
122:단일 client 환경(client-level role 분리 의미 적음)

$ grep -nFo -e '4 패턴 통합 base 로 확장 시 realm 은 공유 1개 유지, client 는 패턴당 1개' -- raw/branch-notes/feature-keycloak-single-ec2-no-google.md
199:4 패턴 통합 base 로 확장 시 realm 은 공유 1개 유지, client 는 패턴당 1개

raw/branch-notes/feature-keycloak-three-leg-trust-chain.md

L4-F11 — 3-leg 표기에는 Backend가 없지만 실제 Hop 3의 검증자는 Backend

  • Severity / 유형: Medium — ambiguous trust-chain taxonomy
  • Source quotes:
    • "P2A는 2-leg trust (Browser ↔ Keycloak)였으나, P2B는 **3-leg trust** (Browser ↔ Keycloak ↔ Google)."...three-leg-trust-chain.md:33
    • "**Hop 3: SPA → Backend**"...three-leg-trust-chain.md:47
  • Claim: WHY의 actor chain은 BrowserKeycloakGoogle만 표시하지만 계약 표는 Google→Keycloak→SPA→Backend의 세 hop과 네 actor를 다룬다.
  • Assumptions: “leg”가 실제 검증 hop을 뜻한다.
  • Failure mode: trust-boundary 그림에서 backend validator가 빠지거나, Browser↔Keycloak leg와 Keycloak→SPA 발급 leg가 서로 다른 방식으로 계산된다.
  • Falsification: “3-leg”가 federation actor 수를 뜻한다면 Hop 3 명칭을 다른 체계로 분리해야 한다.
  • Required action / Why: “3-hop / four-party validation chain”으로 정의하거나 federation chain과 API validation chain을 별도 도식으로 나눈다.
  • Alternatives: Google→Keycloak, Keycloak→SPA, SPA→Backend를 canonical hop 정의로 고정.
  • Counterargument: 제목은 학습용 약칭이다. 신뢰 경계 owner 문서에서는 약칭도 verifier 누락을 만들지 않도록 정의가 필요하다.
  • Self-grep:
$ grep -nFo -e 'P2A는 2-leg trust (Browser ↔ Keycloak)였으나, P2B는 **3-leg trust** (Browser ↔ Keycloak ↔ Google).' -e '**Hop 3: SPA → Backend**' -- raw/branch-notes/feature-keycloak-three-leg-trust-chain.md
33:P2A는 2-leg trust (Browser ↔ Keycloak)였으나, P2B는 **3-leg trust** (Browser ↔ Keycloak ↔ Google).
47:**Hop 3: SPA → Backend**

L4-F12 — 면접 답변은 Keycloak nonce 자동 처리를 사실로 말하지만 D3는 미검증

  • Severity / 유형: Medium — evidence-status contradiction
  • Source quotes:
    • "\nonce`를 확인합니다."...three-leg-trust-chain.md:36`
    • "Keycloak 의 *자동 처리 동작*은 미검증"...three-leg-trust-chain.md:120
  • Claim: normative OIDC RP 의무는 확보됐지만 Keycloak adapter가 해당 요청/검증을 자동 수행하는지는 아직 확인하지 않았다. 면접 문장은 이 구분을 제거한다.
  • Assumptions: line 36은 추후 파생 가능한 답변 초안이다.
  • Failure mode: 구현 관측 없이 Keycloak-specific 동작을 외부에 확정적으로 주장한다.
  • Falsification: Keycloak source, 공식 IdP 문서 또는 HAR로 nonce 송신·대조가 확인되면 승격 가능하다.
  • Required action / Why: 검증 전에는 “OIDC RP가 nonce를 검증해야 하며, Keycloak 자동 처리는 확인 예정”으로 제한한다.
  • Alternatives: CV6을 닫은 뒤 현재 문장 복원.
  • Counterargument: 표준 준수 제품이라 예상 가능하다. 표준이 요구하는 것과 특정 버전·설정이 실제 수행하는 것은 별도 증거다.
  • Self-grep:
$ grep -nFo -e '`nonce`를 확인합니다.' -e 'Keycloak 의 *자동 처리 동작*은 미검증' -- raw/branch-notes/feature-keycloak-three-leg-trust-chain.md
36:`nonce`를 확인합니다.
120:Keycloak 의 *자동 처리 동작*은 미검증

L4-F13 — out-of-scope claim/linking 정책을 구체 검증 항목으로 재진술

  • Severity / 유형: Medium — RESTATED_FOREIGN_DECISION
  • Source quotes:
    • "claim mapping — [[raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role]]"...three-leg-trust-chain.md:57
    • "Account Linking — [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]]"...three-leg-trust-chain.md:58
    • "\email_verified`, `hd`, Account Linking 결정"...three-leg-trust-chain.md:95`
  • Claim: 이 노트는 mapper/account-linking을 위임하면서 내부 hop 표에서는 email_verified·hd·linking을 Keycloak이 검증하는 고정 정책처럼 적는다. 이들은 설정/flow 의존이며 owner D 포인터도 없다.
  • Assumptions: 기본 brokering만으로 세 항목이 모두 강제되지는 않는다.
  • Failure mode: hd enforcement 또는 verified-email linking guard가 구성되지 않았는데 trust check가 존재한다고 오인한다.
  • Falsification: unseen owners가 세 항목을 명시적으로 강제하고 이 문서가 정확한 D-row를 가리키면 1줄 요약으로 유지 가능하다.
  • Required action / Why: row를 “First Broker Login + mapper 정책(config-dependent)”으로 낮추고 각 owner D-row만 연결한다.
  • Alternatives: 이 검증들을 본 문서가 소유하려면 out-of-scope 선언을 제거하고 Single-Owner를 재배치해야 한다.
  • Counterargument: 단순 요약이다. 현재 요약은 어떤 검사가 실제 활성인지까지 암시하므로 메커니즘 복제에 해당한다.
  • Self-grep:
$ grep -nFo -e '`email_verified`, `hd`, Account Linking 결정' -e 'claim mapping — [[raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role]]' -e 'Account Linking — [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]]' -- raw/branch-notes/feature-keycloak-three-leg-trust-chain.md
57:claim mapping — [[raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role]]
58:Account Linking — [[raw/branch-notes/feature-keycloak-account-linking-spa-ux]]
95:`email_verified`, `hd`, Account Linking 결정

raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md

L4-F14 — P3A를 nginx+oauth2-proxy edge-auth 패턴으로 잘못 귀속

  • Severity / 유형: High — CONTRADICTION
  • Source quotes:
    • "nginx+oauth2-proxy 확정(P3A)"...traefik-forwardauth-alternative.md:169
    • "client (vanilla JS SPA via nginx static)" / "Browser → Keycloak → Backend Resource Server JWT validation"...single-ec2-no-google.md:33
  • Claim: Traefik 문서는 P3A를 edge ForwardAuth 구성으로 고정하지만 P3A owner는 SPA-direct PKCE + backend JWT validation이며 nginx는 static server다.
  • Assumptions: P3A 최신 재매핑은 AP1 + single-EC2 deployment base다.
  • Failure mode: oauth2-proxy를 잘못 추가해 browser token custody, backend validation 책임, 인증 redirect 흐름이 모두 바뀐다.
  • Falsification: “AP4 edge-auth를 single-EC2 위에 올린 별도 실행 변형”을 뜻했다면 P3A가 아니라 그 조합으로 명명해야 한다.
  • Required action / Why: nginx+oauth2-proxy는 P1A/AP4 owner로 돌리고, single-EC2는 배포 축으로만 참조한다.
  • Alternatives: AP1-on-single-EC2AP4-on-single-EC2를 별도 matrix row로 둔다.
  • Counterargument: 같은 EC2에서 oauth2-proxy도 실행할 수 있다. 물리 배포 가능성과 인증 패턴 정체성은 별도 축이다.
  • Self-grep:
$ grep -nFo -e 'nginx+oauth2-proxy 확정(P3A)' -- raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md
169:nginx+oauth2-proxy 확정(P3A)

$ grep -nFo -e 'client (vanilla JS SPA via nginx static)' -e 'Browser → Keycloak → Backend Resource Server JWT validation' -- raw/branch-notes/feature-keycloak-single-ec2-no-google.md
33:client (vanilla JS SPA via nginx static)
33:Browser → Keycloak → Backend Resource Server JWT validation

L4-F15 — redirect를 middleware가 생성한다고 적었지만 D3는 auth server 응답 전달로 정의

  • Severity / 유형: Medium — STALE_SUMMARY
  • Source quotes:
    • "middleware가 직접 302를 발급"...traefik-forwardauth-alternative.md:97
    • "비 2XX → 인증 서버 응답 그대로 client 에 반환"...traefik-forwardauth-alternative.md:112
  • Claim: ForwardAuth middleware가 redirect를 생성하는 것과 auth service의 302를 전달하는 것은 다른 책임 모델이다.
  • Assumptions: D3의 vendor-backed contract가 정본이다.
  • Failure mode: redirect loop나 Location 손실을 Traefik 설정 문제로만 진단하고 oauth2-proxy 응답을 점검하지 않는다.
  • Falsification: 사용 중인 auth plugin이 Traefik 내부에서 직접 redirect를 생성한다면 별도 D10 경로로 분리해야 한다.
  • Required action / Why: line 97을 “auth server의 302를 Traefik이 그대로 전달”로 갱신한다.
  • Alternatives: oauth2-proxy 외부 서버와 in-process OIDC plugin의 redirect owner를 표에서 분리한다.
  • Counterargument: browser 입장에서는 Traefik에서 302가 온다. 그러나 장애 분석에는 302 생성 주체와 전달 주체 구분이 필요하다.
  • Self-grep:
$ grep -nFo -e 'middleware가 직접 302를 발급' -e '비 2XX → 인증 서버 응답 그대로 client 에 반환' -- raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md
97:middleware가 직접 302를 발급
112:비 2XX → 인증 서버 응답 그대로 client 에 반환

raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md

L4-F16 — relative /api/me·/token URL이 static-only nginx topology에서 잘못된 origin으로 감

  • Severity / 유형: High — CONTRADICTION
  • Source quotes:
    • "fetch('/api/me', { headers: { Authorization: 'Bearer ' + user.access_token } })"...vanilla-js-spa-pkce.md:176
    • "manual \fetch('/token'"...vanilla-js-spa-pkce.md:190`
    • "nginx는 단순 static 파일 서빙"...single-ec2-no-google.md:159
  • Claim: SPA origin이 http://localhost이고 nginx가 static-only이면 두 relative URL은 nginx:80으로 향한다. 문서의 backend:8081·Keycloak:8080 직노출 topology와 양립하지 않는다.
  • Assumptions: nginx에 /api·/token reverse proxy가 없다.
  • Failure mode: API는 404, token exchange는 404/405가 되어 E2E가 시작되지 않는다.
  • Falsification: 실제 nginx config가 두 path를 proxy한다면 parent의 static-only·3-port/CORS 계약이 stale다.
  • Required action / Why: direct topology면 absolute http://localhost:8081/api/me와 discovered Keycloak token endpoint를 사용하고, same-origin topology면 nginx proxy를 owner 결정으로 승격한다.
  • Alternatives:
    1. 3-port direct + CORS
    2. nginx /api·OIDC proxy + same-origin
    3. runtime config에서 endpoint base URL 주입
  • Counterargument: dev proxy가 상대 URL을 처리할 수 있다. 현재 산출물은 vanilla static nginx이며 dev proxy가 명세에 없다.
  • Self-grep:
$ grep -nFo -e 'fetch('\''/api/me'\'', { headers: { Authorization: '\''Bearer '\'' + user.access_token } })' -e 'manual `fetch('\''/token'\''' -- raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md
84:fetch('/api/me', { headers: { Authorization: 'Bearer ' + user.access_token } })
176:fetch('/api/me', { headers: { Authorization: 'Bearer ' + user.access_token } })
190:manual `fetch('/token'

$ grep -nFo -e 'nginx는 단순 static 파일 서빙' -- raw/branch-notes/feature-keycloak-single-ec2-no-google.md
159:nginx는 단순 static 파일 서빙

L4-F17 — oidc-client-ts default가 localStorage라는 단정과 “미증명” 상태가 공존

  • Severity / 유형: Medium — evidence-status CONTRADICTION
  • Source quotes:
    • "oidc-client-ts 기본은 \WebStorageStateStore` (localStorage)."...vanilla-js-spa-pkce.md:98`
    • "default store 종류(localStorage 여부)는 본 Sources 로 미증명"...vanilla-js-spa-pkce.md:168
  • Claim: 같은 파일이 library default를 사실과 미확인으로 동시에 표시한다. stateStoreuserStore default도 구분되지 않았다.
  • Assumptions: library version별 default가 달라질 수 있고 현재 raw source가 이를 고정하지 않는다.
  • Failure mode: 불필요하거나 잘못된 override로 redirect transaction state가 소실되거나, 실제 token이 persistent storage에 남는다.
  • Falsification: pin된 oidc-client-ts 버전 공식 API/default 문서가 localStorage를 명시하면 첫 문장을 근거화할 수 있다.
  • Required action / Why: 검증 전 line 98을 inference로 낮추고 두 store의 default와 token 저장 여부를 각각 확인한다.
  • Alternatives: 명시적 custom store로 version default에 의존하지 않되 full-page redirect state 생존 테스트를 추가한다.
  • Counterargument: 어차피 memory override를 쓸 예정이다. stateStore까지 memory로 바꾸면 callback state가 사라질 수 있어 default 확인이 여전히 필요하다.
  • Self-grep:
$ grep -nFo -e 'oidc-client-ts 기본은 `WebStorageStateStore` (localStorage).' -e 'default store 종류(localStorage 여부)는 본 Sources 로 미증명' -- raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md
98:oidc-client-ts 기본은 `WebStorageStateStore` (localStorage).
168:default store 종류(localStorage 여부)는 본 Sources 로 미증명

L4-F18 — iframe silent SSO는 out-of-scope인데 automaticSilentRenew 메커니즘은 미결정

  • Severity / 유형: Medium — ambiguous flow/default
  • Source quotes:
    • "iframe 기반 silent SSO (deprecated, 대신 refresh token 사용)"...vanilla-js-spa-pkce.md:58
    • "어느 메커니즘이 default 인지 본 인용 범위 밖"...vanilla-js-spa-pkce.md:217
  • Claim: iframe을 제외하고 refresh grant를 채택했다고 쓰면서 실제 library가 iframe과 refresh grant 중 무엇을 쓰는지 확인하지 않았다.
  • Assumptions: E2E acceptance에는 reload·expiry 갱신 경로가 포함된다.
  • Failure mode: Safari/cross-site에서 silent renew 실패, reload 후 in-memory user 복구 실패, 또는 rotation 시연이 실행되지 않는다.
  • Falsification: pin된 library 설정과 network trace로 refresh-token grant만 사용함을 확인하면 out-of-scope 선언과 정합해진다.
  • Required action / Why: refresh grant를 baseline으로 고정하고 필요한 scope/client 설정·reload bootstrap을 명세하거나 iframe을 조건부 경로로 다시 scope에 넣는다.
  • Alternatives: memory-only + 명시적 재로그인.
  • Counterargument: library가 런타임에 적절한 방식을 택한다. 브라우저 정책에 따라 결과가 달라지므로 학습 E2E에는 결정적 기대 동작이 필요하다.
  • Self-grep:
$ grep -nFo -e 'iframe 기반 silent SSO (deprecated, 대신 refresh token 사용)' -e '어느 메커니즘이 default 인지 본 인용 범위 밖' -- raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md
58:iframe 기반 silent SSO (deprecated, 대신 refresh token 사용)
217:어느 메커니즘이 default 인지 본 인용 범위 밖

raw/branch-notes/feature-security-operational-baseline.md

L4-F19 — Phase C2 완료 뒤 active 구현 표가 codes·CORS·redaction을 계속 미구현으로 표시

  • Severity / 유형: High — STALE_SUMMARY
  • Source quotes:
    • "production enum \OperationalError` 미구현"...feature-security-operational-baseline.md:222`
    • "CorsSettings\ 가 `["*"]`+`allowCredentials=true` 조합을 startup 거부하지 않음"...feature-security-operational-baseline.md:269`
    • "token leak contract test" (planned) — ...feature-security-operational-baseline.md:279
    • "12 fine-grained AUTH/AUTHZ/INTERNAL codes"...feature-security-operational-baseline.md:360
    • "token/PII redaction"...feature-security-operational-baseline.md:363
    • "CORS wildcard+credentials 정적 거부"...feature-security-operational-baseline.md:365
    • CODE_GRANULARITY_DRIFT ... RESOLVED...feature-security-operational-baseline.md:372
  • Claim: Phase C2 as-built 표는 세 항목을 locally verified로 승격했지만 앞선 Implementation Specification은 여전히 planned/미구현 상태다.
  • Assumptions: Phase C2 표가 최신 코드 대조 결과다.
  • Failure mode: 동일 기능을 재구현하거나 canonical 추출에서 구현 사실을 누락하고, 현재 테스트 범위를 잘못 판단한다.
  • Falsification: Phase C2 표가 이후 revert된 상태라면 해당 표를 downgrade해야 한다.
  • Required action / Why: active 구현 가이드와 Claims를 Phase C2 as-built에 맞추고 과거 상태는 Audit history로만 남긴다.
  • Alternatives: 각 오래된 표에 “pre-2026-06-08 historical” 라벨을 붙인다.
  • Counterargument: 아래 Audit에서 해소 사실을 찾을 수 있다. 상단 active specification과 검증 큐가 stale이면 소비자는 끝까지 읽지 않는 한 반대 결론을 얻는다.
  • Self-grep:
$ grep -nFo -e 'production enum `OperationalError` 미구현' -e '12 fine-grained AUTH/AUTHZ/INTERNAL codes' -e 'CODE_GRANULARITY_DRIFT' -e 'CorsSettings` 가 `["*"]`+`allowCredentials=true` 조합을 startup 거부하지 **않음**' -e 'token leak contract test' -e 'CORS wildcard+credentials 정적 거부' -e 'token/PII redaction' -- raw/branch-notes/feature-security-operational-baseline.md
144:CODE_GRANULARITY_DRIFT
214:CODE_GRANULARITY_DRIFT
222:production enum `OperationalError` 미구현
269:CorsSettings` 가 `["*"]`+`allowCredentials=true` 조합을 startup 거부하지 **않음**
279:token leak contract test
317:CODE_GRANULARITY_DRIFT
360:12 fine-grained AUTH/AUTHZ/INTERNAL codes
363:token/PII redaction
365:CORS wildcard+credentials 정적 거부
372:CODE_GRANULARITY_DRIFT
372:12 fine-grained AUTH/AUTHZ/INTERNAL codes

L4-F20 — JWT decoder와 clock-skew의 현재 메커니즘이 세 가지 상태로 기록

  • Severity / 유형: High — CONTRADICTION
  • Source quotes:
    • "auto-config** (custom decoder 미작성)"...feature-security-operational-baseline.md:197
    • "custom \JwtDecoder` bean"...feature-security-operational-baseline.md:364`
    • "60s 는 코드에 명시되지 않고 Spring default leeway 에 의존"...feature-security-operational-baseline.md:374
  • Claim: active guide는 auto-config/default skew, Phase C2 표는 custom decoder와 explicit skew, 후속 Audit bullet은 다시 명시 설정 부재라고 말한다.
  • Assumptions: 세 진술 모두 같은 2026-06-08 이후 코드 상태를 설명한다.
  • Failure mode: 잘못된 bean을 수정하거나 default drift 위험·startup IdP 의존성·validator chain 테스트 범위를 오판한다.
  • Falsification: SupplierJwtDecoder가 custom wrapper이지만 내부적으로 auto-config/default skew를 그대로 사용한다면 line 364의 “explicit clock skew” 표현이 잘못된 것이다.
  • Required action / Why: 실제 code owner에서 JwtDecoderConfig의 validator chain을 대조한 뒤 한 상태만 남긴다.
  • Alternatives:
    1. custom decoder + explicit JwtTimestampValidator(60s)
    2. auto-config + vendor default 60s
  • Counterargument: custom wrapper와 auto-config를 함께 쓸 수 있다. 그래도 “custom decoder 미작성”과 “custom bean locally verified”는 동시에 참일 수 없다.
  • Self-grep:
$ grep -nFo -e 'auto-config** (custom decoder 미작성)' -e 'custom `JwtDecoder` bean' -e '60s 는 코드에 명시되지 않고 Spring default leeway 에 의존' -- raw/branch-notes/feature-security-operational-baseline.md
197:auto-config** (custom decoder 미작성)
364:custom `JwtDecoder` bean
374:60s 는 코드에 명시되지 않고 Spring default leeway 에 의존

L4-F21 — public-path 검증 계약이 폐기된 reflection 방식과 구현된 env-snapshot 방식을 함께 유지

  • Severity / 유형: High — STALE_SUMMARY / incompatible verification command
  • Source quotes:
    • "SecurityFilterChain.getFilters()\ introspection"...feature-security-operational-baseline.md:305`
    • "gradle task 미존재"...feature-security-operational-baseline.md:318
    • "public path snapshot gate" (locally-verified) — ...feature-security-operational-baseline.md:366
  • Claim: 구현 가이드 line 243250은 env-based snapshot으로 정정됐지만 Test Contract와 Claims는 reflection 및 task 부재를 계속 전제로 한다.
  • Assumptions: Phase C2의 verifyPublicPathSnapshot task가 현행이다.
  • Failure mode: 존재하는 task를 다시 작성하거나 reflection coverage를 기대해 Java hard-coded permitAll()까지 탐지된다고 오판한다.
  • Falsification: task가 이후 제거됐다면 Phase C2와 구현 가이드를 downgrade해야 한다.
  • Required action / Why: Test Contract와 Claims를 env SECURITY_PUBLIC_PATHS snapshot command·승인 flag·hard-coded permitAll() 미검출 한계로 동기화한다.
  • Alternatives: 별도 reflection/behavioral test를 추가해 env gate의 blind spot을 보완한다.
  • Counterargument: line 243에 정정 설명이 있다. 하지만 실행 명령을 제공하는 §테스트 계약과 active Claims가 반대 상태여서 운영자가 잘못된 검증을 수행한다.
  • Self-grep:
$ grep -nFo -e 'SecurityFilterChain.getFilters()` introspection' -e 'gradle task 미존재' -e 'public path snapshot gate' -- raw/branch-notes/feature-security-operational-baseline.md
243:SecurityFilterChain.getFilters()` introspection
305:SecurityFilterChain.getFilters()` introspection
318:gradle task 미존재
318:SecurityFilterChain.getFilters()` introspection
366:public path snapshot gate

Cross-lane owner edges

L4 finding Unseen owner / governing source 필요한 대조 상태
L4-F01 feature-keycloak-iss-claim-hostname-mismatch D6, feature-keycloak-docker-compose-stack bridge/host network 기본과 issuer-uri/jwk-set-uri 조합 NEEDS_CROSS_LANE_AUDIT
L4-F02, F10, F14 raw/project-notes/keycloak-patterns-overview F3/F5/§8 최신 AP taxonomy·owner split·single-EC2 deployment 축 NEEDS_CROSS_LANE_AUDIT
L4-F04, F06 feature-keycloak-internal-spa-direct-no-google, feature-keycloak-bff-vs-spa-direct TMB 허용 여부와 cookie/CSRF owner NEEDS_CROSS_LANE_AUDIT
L4-F08 feature-keycloak-realm-client-export 실제 client IDs와 Audience mapper target NEEDS_CROSS_LANE_AUDIT
L4-F10 feature-keycloak-realm-client-export D5 realm roles 대 client roles의 발급 계약 NEEDS_CROSS_LANE_AUDIT
L4-F12, F13 feature-keycloak-first-broker-login-flow, feature-keycloak-idp-mappers-claim-to-role, feature-keycloak-account-linking-spa-ux nonce 자동 처리·email_verified·hd·linking 실제 정책 NEEDS_CROSS_LANE_AUDIT
L4-F14, F15 feature-keycloak-edge-forwardauth-no-google, feature-keycloak-oauth2-proxy-oidc-flow AP4 owner 및 302 생성/전달 주체 NEEDS_CROSS_LANE_AUDIT
L4-F18 feature-keycloak-refresh-token-rotation, feature-keycloak-refresh-rotation-and-logout refresh grant·rotation·reload 복구 계약 NEEDS_CROSS_LANE_AUDIT
L4-F19F21 wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets.md + ca-tmpl code owner Phase C2 as-built와 canonical/current code 정합 NEEDS_CROSS_LANE_AUDIT

Claim Traceability Check

  • Claim ID 연결 검사: 8개 branch-note의 Supporting Claims 구조를 모두 확인했다. exact slice 밖 raw-source Claim 본문은 재열람하지 않았으므로 외부 Claim byte 검증은 범위 밖이다. slice 내부 owner 의미 오귀속은 L4-F02·F06·F09로 보고했다.
  • Decision Evidence Map: 8개 모두 검토. 최신 D-row와 본문/소비자 요약의 semantic drift는 L4-F02·F03·F05·F07F10·F12·F19F21에 반영했다.
  • UNSUPPORTED_DECISION: 새 미라벨 결정으로 확정한 건은 없으며, 기존 label을 무시한 상위 요약 과장은 L4-F12에 포함했다.

Grep verification

  • 실행한 grep -nFo self-check: 25회
  • 검증한 verbatim quote fragment V: 55
  • 일치 P: 55
  • 라인 정정 C: 0
  • 폐기 D: 0
  • 모든 command exit code: 0
  • 이 lane은 파일 수정 명령을 실행하지 않았다. 공유 worktree에는 선행 변경이 존재하므로 workspace-clean 주장은 하지 않는다.

Stats

agent: wiki-research-lane
found: 8
processed: 8
dropped: 0