init: llm-wiki-haness 하네스 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:21:35 +09:00
parent 42bf3db4fd
commit 6c53ded9cb
2436 changed files with 194486 additions and 1 deletions
@@ -0,0 +1,57 @@
# Priority Recommendations
적대 리뷰에서 살아남은 High 10건만 우선순위에 올렸다. 각 행은 하나의 finding과 1:1로 대응한다. 원문 수정은 아직 승인되지 않았다.
| Priority | Finding | Why Now | Concrete Change | Verification | Risk / Approval |
|---:|---|---|---|---|---|
| 1 | L4-F08 | mapper가 넣는 API audience와 backend expected audience가 다르면 정상 token이 401이 되거나 다른 resource token을 허용할 수 있다. | realm client ID, API audience symbolic name, mapper target, Spring audiences 값을 한 owner D-row에서 하나로 고정하고 role-mapping의 복제 값을 pointer로 바꾼다. | realm export, 발급 token aud, 정상 및 wrong-audience 401 test를 함께 확인한다. | high / 개별 승인 |
| 2 | L1-F08 | 선택된 bridge-network 기본이 localhost discovery와 host.docker.internal reachability를 동시에 요구한다. | issuer identity와 JWKS network address를 분리하거나 host network를 기본으로 택해 한 실행 profile로 종결한다. | docker compose config, app container DNS, startup discovery와 JWKS log를 확인한다. | high / 개별 승인 |
| 3 | L4-F16 | copyable SPA code의 relative API와 token URL이 static-only nginx origin으로 향한다. | nginx에 명시적 proxy route를 추가하거나 SPA config에 backend와 Keycloak absolute base URL을 주입하고 topology owner에서 한 방식을 선택한다. | browser Network panel, nginx access log, backend와 Keycloak log로 resolved destination을 확인한다. | high / 개별 승인 |
| 4 | L3-F14 | Caddy handle_path가 Keycloak relative path를 제거해 discovery와 authorization endpoint가 404가 될 수 있다. | method B를 유지하면 handle 또는 prefix-preserving reverse_proxy로 snippet을 바꾸고, strip 방식을 택하면 Keycloak root-path 설계로 owner decision을 바꾼다. | caddy adapt와 upstream request path로 discovery, auth, token endpoint를 검사한다. | high / 개별 승인 |
| 5 | L2-F02 | silent auto-link 차단과 email_verified=false 전체 hard-reject가 같은 acceptance처럼 쓰인다. | First Broker Login owner에서 silent-link policy와 custom SPI hard-reject policy를 분리하고, SPI artifact가 없으면 consumer의 링크와 생성 거부 문장을 제거한다. | realm flow export, provider JAR, false-email 로그인 E2E로 선택 정책을 증명한다. | high / 개별 승인 |
| 6 | L4-F04 | HttpOnly refresh cookie를 기본으로 두면 server-side token custody와 CSRF 책임이 생겨 SPA Direct 경계가 바뀐다. | pure SPA memory-only와 TMB refresh-cookie variant를 별도 decision으로 나누고 기본 하나와 fallback 조건을 고정한다. | refresh endpoint와 cookie issuer 존재, CSRF negative test, reload 및 rotation E2E를 확인한다. | high / 개별 승인 |
| 7 | L1-F10 | P1B 표는 P1A와 동일이라 적지만 backend trust는 JWT signature와 header-only로 갈린다. | P1B가 P1A trust invariant를 유지할지 새 JWT-validation variant가 될지 선택하고 pattern ID, component table, security tests를 함께 갱신한다. | 두 pattern의 SecurityFilterChain, forwarded headers와 tokens, spoofing negative test를 diff한다. | high / 개별 승인 |
| 8 | L4-F14 | Traefik note가 SPA Direct P3A를 edge-auth AP4로 연결해 배포 축과 인증 architecture 축을 섞는다. | target pattern pointer를 실제 AP owner로 바꾸고 physical deployment alternative는 별도 행으로 분리한다. | project pattern matrix와 component flow에서 browser token holder와 backend verifier가 일치하는지 확인한다. | high / 개별 승인 |
| 9 | L3-F08 | random quick-tunnel URL을 static redirect hostname처럼 사용하면 restart 뒤 Google callback 등록이 stale해진다. | quick tunnel과 named tunnel plus managed hostname을 분리하고 static callback 요구 시 허용되는 provider profile을 하나로 고정한다. | tunnel restart 전후 hostname, DNS route, Google redirect E2E를 기록한다. | high / 개별 승인 |
| 10 | L1-F09 | Google sub federation key의 변경 권한이 parent와 child 양쪽 SSOT에 있다. | parent는 requirement만 두고 child D1을 mechanism owner로 삼거나, 반대로 parent owner를 유지하고 child를 pointer와 implementation detail로 축소한다. | inbound decision reference와 owner map에서 mutable key policy owner가 하나인지 확인한다. | high / 개별 승인 |
## Decision Conditions
| Finding | Goal | Assumption and invalidation | Strongest counterargument | Decision |
|---|---|---|---|---|
| L4-F08 | API token acceptance와 resource isolation을 같은 audience contract로 묶는다. | spa-client와 backend-client-id가 별도 client라는 전제다. realm export에서 동일 identifier로 의도적으로 정의됐다면 값 충돌은 사라진다. | public client ID를 API audience로 재사용할 수 있다. | identifier 재사용 여부를 owner에서 명시하고 mapper와 validator에 같은 symbol을 사용한다. |
| L1-F08 | issuer identity와 container reachability가 함께 성립하는 기본을 만든다. | backend가 bridge container라는 전제다. host process나 host network가 실제 기본이면 반론이 성립한다. | extra_hosts로 host 접근이 가능하다. | extra_hosts hostname과 issuer hostname은 자동으로 같아지지 않으므로 rendered config를 기준으로 profile을 고정한다. |
| L4-F16 | copyable SPA request가 의도한 origin으로 간다. | nginx가 static-only라는 parent 결정이 전제다. 실제 proxy route가 있으면 finding은 약해진다. | relative URL은 same-origin 배포에서 단순하다. | same-origin proxy를 문서와 config에 추가하거나 absolute runtime config를 쓴다. |
| L3-F14 | proxy path와 Keycloak relative path를 같은 routing contract로 만든다. | Caddy handle_path의 prefix strip과 method B가 동시에 선택됐다는 전제다. 별도 rewrite가 있으면 반론이 가능하다. | upstream에서 prefix를 복구할 수 있다. | 보이지 않는 rewrite에 의존하지 말고 copyable snippet에 선택을 표현한다. |
| L2-F02 | account-link policy의 실제 거부 범위를 acceptance와 맞춘다. | custom hard-reject SPI가 corpus에 없다는 전제다. 별도 artifact가 확인되면 consumer 문장을 유지할 수 있다. | Google verified email이 일반적이라 edge case가 드물다. | 빈도와 보안 계약은 별개이므로 flow export와 negative test로 범위를 고정한다. |
| L4-F04 | token custody에 맞는 architecture와 CSRF contract를 선택한다. | HttpOnly refresh cookie를 실제 기본으로 쓴다는 전제다. memory-only를 고르면 TMB concern이 사라진다. | 기존 Resource Server에 endpoint 하나만 추가할 수 있다. | endpoint 수보다 custody와 browser-triggered credential 책임을 기준으로 pattern을 분류한다. |
| L1-F10 | P1A와 P1B의 backend trust boundary를 명시한다. | 동일이라는 표가 security invariant까지 포함한다는 전제다. federation 부분만 동일하다고 범위를 좁히면 별도 variant가 가능하다. | defense-in-depth JWT validation을 P1B에 추가할 수 있다. | 그 경우 동일 표현을 제거하고 새 pattern invariant와 test를 둔다. |
| L4-F14 | pattern taxonomy와 배포 alternative를 서로 다른 축으로 유지한다. | P3A owner가 SPA Direct라는 전제다. project hub가 이미 AP4로 재분류했다면 owner 문서를 먼저 바꿔야 한다. | 같은 EC2에 oauth2-proxy를 추가할 수 있다. | 배치 가능성은 architecture ID 변경 근거가 아니므로 두 축을 분리한다. |
| L3-F08 | redirect URI가 tunnel lifecycle 동안 안정적으로 유지되게 한다. | quick tunnel URL이 random이라는 보존 source model이 전제다. target account가 persistent hostname을 제공하면 profile을 수정할 수 있다. | 특정 plan이나 managed domain은 static일 수 있다. | quick, named, managed custom hostname을 별도 조건으로 문서화한다. |
| L1-F09 | federation key 변경 권한을 한 owner에 둔다. | 두 D-row가 같은 mutable policy를 결정한다는 전제다. requirement와 mechanism concern ID가 분리되면 양립한다. | integration parent가 requirement를 반복할 수 있다. | 값과 선택 조건은 child owner에 두고 parent는 invariant pointer만 유지한다. |
## Fix Plan
| Finding | 필요한 수정 | 대상 파일:line | 위험 | 승인 필요? | 패치 범위 |
|---|---|---|---|---|---|
| L4-F08 | audience identifier와 owner 통합 | `raw/branch-notes/feature-keycloak-spring-rs-audience-validator.md:156`, `raw/branch-notes/feature-keycloak-spring-rs-role-mapping.md:38` | high | yes, 개별 | D-row, mapper, validator, test contract |
| L1-F08 | issuer와 network profile 한 가지 선택 | `raw/branch-notes/feature-keycloak-docker-compose-stack.md:138`, `raw/branch-notes/feature-keycloak-docker-compose-stack.md:207` | high | yes, 개별 | Compose 및 D3 관련 문단 |
| L4-F16 | SPA endpoint routing 방식 선택 | `raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md:176`, `raw/branch-notes/feature-keycloak-vanilla-js-spa-pkce.md:190`, `raw/branch-notes/feature-keycloak-single-ec2-no-google.md:159` | high | yes, 개별 | code snippet과 topology decision |
| L3-F14 | Caddy prefix-preserving snippet 적용 | `raw/branch-notes/feature-keycloak-reverse-proxy-headers.md:125`, `raw/branch-notes/feature-keycloak-reverse-proxy-headers.md:202` | high | yes, 개별 | Caddy snippet과 D3 trace |
| L2-F02 | silent-link와 hard-reject 분리 | `raw/branch-notes/feature-keycloak-first-broker-login-flow.md:135`, `raw/branch-notes/feature-keycloak-idp-mappers-claim-to-role.md:120`, `raw/branch-notes/feature-keycloak-internal-spa-direct-google-federation.md:387` | high | yes, 개별 | policy D-row와 consumer summaries |
| L4-F04 | pure SPA와 TMB variant 분리 | `raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md:140`, `raw/branch-notes/feature-keycloak-spa-token-storage-tradeoff.md:165` | high | yes, 개별 | D1, architecture taxonomy, CSRF owner |
| L1-F10 | P1B backend trust variant 확정 | `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:145`, `raw/branch-notes/feature-keycloak-edge-forwardauth-no-google.md:112` | high | yes, 개별 | comparison table와 security invariant |
| L4-F14 | P3A pointer를 실제 pattern owner로 교체 | `raw/branch-notes/feature-keycloak-traefik-forwardauth-alternative.md:169`, `raw/branch-notes/feature-keycloak-single-ec2-no-google.md:33` | medium | yes, 개별 | taxonomy pointer와 설명 |
| L3-F08 | tunnel hostname 유형 분리 | `raw/branch-notes/feature-keycloak-public-domain-tunneling.md:69`, `raw/branch-notes/feature-keycloak-public-domain-tunneling.md:77`, `raw/branch-notes/feature-keycloak-single-ec2-google-federation.md:356` | medium | yes, 개별 | provider matrix와 redirect assumptions |
| L1-F09 | sub key owner 하나 선택 | `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:195`, `raw/branch-notes/feature-keycloak-edge-forwardauth-google-federation.md:253`, `raw/branch-notes/feature-keycloak-account-linking-sub-vs-email.md:150` | high | yes, 개별 | owner map와 D-row |
| L3-F04 | bare D-reference를 wikilink plus D-id로 교체 | `raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow.md:214`, `raw/branch-notes/feature-keycloak-oauth2-proxy-oidc-flow.md:228`, `raw/branch-notes/feature-keycloak-realm-client-export.md:182` 및 deterministic warning 5건 | low | yes, 묶음 | 8개 reference syntax |
| L3-F11,L3-F12,L3-F13 | rotation owner, receiver scope, Max Reuse 표현을 한 계약에서 검증 | `raw/branch-notes/feature-keycloak-refresh-token-rotation.md:159`, `raw/branch-notes/feature-keycloak-refresh-rotation-and-logout.md:124` | medium | yes, 묶음 | D4/D5, scope, measured semantics |
| L4-F19,L4-F20,L4-F21 | Phase C2 as-built 기준으로 history와 active spec 분리 | `raw/branch-notes/feature-security-operational-baseline.md:243` 및 Phase C2 sections | medium | yes, as-built 확인 후 | active table, Claims, Test Contract |
## Verification order
1. Realm export와 token sample로 L4-F08을 먼저 닫는다.
2. Rendered Compose와 proxy config로 L1-F08, L3-F14, L4-F16을 함께 검증한다.
3. First Broker Login export와 negative E2E로 L2-F02를 확인한다.
4. Token storage architecture 선택 뒤 L4-F04의 CSRF와 rotation owner를 정한다.
5. 나머지 ownership 및 taxonomy cleanup을 적용하고 consistency checker를 다시 실행한다.