53 KiB
title, source_type, status, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, last_implementation, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, contract_packet_sha256
| title | source_type | status | branch | parent_branch | related_projects | governing_docs | tags | created | target_merge | status_label | last_implementation | id | kind | project | work_item | inherits | refines | overrides | depends_on | contract_packet | contract_packet_sha256 | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| branch / feature-security-operational-baseline | branch-note | raw | feature-security-operational-baseline |
|
|
|
2026-05-21 | in-progress | 2026-06-08 | BR-CA-SKELETON-OPERATIONAL-CONTRACT-008 | project-work-item | ca-skeleton-operational-contract | WI-CA-SKELETON-OPERATIONAL-CONTRACT-008 |
|
1 | 4aeeee0f8f366a32a08f4e6c687e7bf7bab5de74f51c5ca05069b8cdb6cf7cfe |
현재 알려진 최신 상태: 2026-06-08 Phase C2 기록은 아래 여러 항목을
locally-verified로 보고한다. 다만 현재 wiki workspace에는 해당src/code owner가 없어 이번 정합 작업에서 재검증하지 못했다. 따라서 active 표는 Phase C2 보고값과 현행 코드 재확인 필요를 함께 표시하며, pre-C2 표·명령은 historical/superseded로 본다.
branch: feature-security-operational-baseline
Layer:
raw/branch-notes/— JWT Resource Server 기준의 인증/인가 실패 운영 분류를 정의합니다.
부모 (필수)
- Parent project (canonical SSOT): raw/project-notes/ca-skeleton-operational-contract
ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: security failure·header contract와 negative test가 통과한다
상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1 |
framework는 Spring Boot 3.5.14다 | Work Item 완료 조건에 적용 | raw/project-notes/ca-skeleton-operational-contract |
브랜치 지역 결정
기존 branch-local 결정은 아래
## Decision Evidence Map / 결정-근거 매핑의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|
선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|
목표
Security 실패를 401/403으로만 처리하면 운영자가 missing token, expired token, issuer mismatch, public path misconfiguration을 구분할 수 없습니다. 클라이언트 응답은 과노출하지 않고 내부 로그에는 안전한 분류 code를 남깁니다.
- 이슈:
- PR:
범위
포함 범위
- JWT Resource Server baseline.
- missing/malformed/expired token 분류.
- invalid signature/issuer/audience 분류.
- claim mapping failure 분류.
- public path misconfiguration 테스트 기준.
- CORS rejection log 기준.
- token/PII 로그 금지.
제외 범위
- OAuth authorization server 구현.
- session 기반 security.
- business role/permission model.
근거 (필수, 최소 1개+)
본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.
| Source | 정당화하는 결정 |
|---|---|
| raw/official-docs/security-jwt-rfc-7519-validation | RFC 7519 claim 검증 표준 (ca-tmpl clock skew 60s가 RFC 권고 "a few minutes leeway" 내 |
| raw/official-docs/security-authorization-cheatsheet-owasp | OWASP deny-by-default 원칙 |
| raw/official-docs/security-oauth2-pkce-rfc-8252 | issuance flow 영역, JWT 검증과 보완재 관계 |
| raw/official-docs/security-mtls-rfc-8705 | sender-constrained로 강함 vs PKI 운영 부담 + public client 미지원 |
| raw/official-docs/security-aws-sigv4-hmac-signing | webhook 검증 같은 영역 한정 |
| raw/official-docs/security-opa-policy-engine-official | 정책-코드 분리 강점 vs latency·운영 부담; ca-tmpl AUTHZ 2종은 in-process 충분 |
| raw/company-tech-blogs/security-toss-actuator-healthcheck | 토스 — health 정보의 민감성 분류 |
| raw/official-docs/owasp-file-upload-cheat-sheet | OWASP file upload 방어 원칙 (extension allowlist, Content-Type 신뢰 금지, UUID 파일명, webroot 밖 저장, size limit, AV 스캔, least-privilege) — upload endpoint 의 deny-by-default 운영 baseline 보강. 본 branch 의 JWT/CORS 결정에는 직접 연결되지 않으며, 파일 처리 상세는 raw/branch-notes/feature-file-resource-handling-contract 소관 |
| raw/official-docs/fetch-spec-cors | WHATWG Fetch §3.3 CORS protocol — D9 (CORS allowlist + credentials false default + max-age + wildcard+credentials 금지) 의 1차 normative 근거. FETCH-CORS-C3: credentials=include 시 Access-Control-Allow-Origin=* 금지. FETCH-CORS-C5: max-age 기본 5초. D9 UNSUPPORTED_DECISION 해소 — official-standard |
| raw/official-docs/jwks-nimbus-jose-jwksourcebuilder-spring-integration | D10 JWKS refresh 메커니즘: Spring 기본 cache 5min, withJwkSetUri() 기본 rateLimited(false)/refreshAheadCache(false), unknown kid → cache.invalidate() (NIMBUS-JWKS-C4/C5/C6) — official-vendor-doc. 10min/1min exact number 는 미증명 |
| raw/official-docs/jwks-keycloak-key-rotation-active-passive | D10 rotation overlap 의 IdP-side 근거: Keycloak active/passive key model + 권고 rotation 주기 (KC-ROT-C1~C6) — official-vendor-doc |
| raw/company-tech-blogs/jwks-workos-unknown-kid-refresh-rate-limit-pattern | D10 unknown kid refetch-before-reject + rate-limit 5company-case-study (best practice 승격 금지; ca-tmpl 1/min 은 이보다 짧아 trade-off 명시) |
| raw/official-docs/rfc9110-http-semantics | D7 401/403 HTTP semantics: §15.5.2 401(인증 자격 부재 + WWW-Authenticate MUST, RFC9110-C23) + §15.5.4 403(자격 불충분, RFC9110-C24) — official-standard. AuthN matrix 401 행 / AUTHZ matrix 403 행의 normative 근거 |
| raw/official-docs/security-spring-jwt-timestamp-validator-clock-skew | D2 clock skew 60s: Spring Security Resource Server default clock skew = 60초 (SS-JTVC-C1) — official-vendor-doc. 코드의 default-의존을 벤더 doc 으로 확정 |
외부 근거 / 대안 조사 (2026-05-22 — Group G-B: Security Baseline)
본 branch의 JWT Resource Server + AuthN/AuthZ Decision Matrix 12행 + JWKS 10min refresh + clock skew 60s + rotation overlap 24h 결정에 대한 외부 source.
- 채택 결정 (JWT Resource Server + RFC 7519 + deny-by-default):
- raw/official-docs/security-jwt-rfc-7519-validation — RFC 7519 claim 검증 표준 (ca-tmpl clock skew 60s가 RFC 권고 "a few minutes leeway" 내)
- raw/official-docs/security-authorization-cheatsheet-owasp — OWASP deny-by-default 원칙
- 검토한 대안:
- 대안 1: Session+cookie — stateless 확장성 손실 + revocation 용이 (ca-tmpl scope 부적합)
- 대안 2: OAuth2 Authorization Code + PKCE — raw/official-docs/security-oauth2-pkce-rfc-8252 (issuance flow 영역, JWT 검증과 보완재 관계)
- 대안 3: mTLS — raw/official-docs/security-mtls-rfc-8705 (sender-constrained로 강함 vs PKI 운영 부담 + public client 미지원)
- 대안 4: HMAC SigV4 — raw/official-docs/security-aws-sigv4-hmac-signing (webhook 검증 같은 영역 한정)
- 대안 5: OPA policy engine — raw/official-docs/security-opa-policy-engine-official (정책-코드 분리 강점 vs latency·운영 부담; ca-tmpl AUTHZ 2종은 in-process 충분)
- 비교 핵심: ca-tmpl JWT Resource Server는 stateless 확장성 우위 + RFC 7519 + JWKS rotation으로 일부 revocation 회수. mTLS/OPA는 강하지만 skeleton 단계 운영 부담 큼. SigV4는 외부 webhook 한정. OAuth2 PKCE는 issuance flow라 보완재.
후속 보강 (2026-05-22): 한국 보안 사례 source 추가 (public path / health detail 노출 관점). raw/company-tech-blogs/security-toss-actuator-healthcheck (토스 — health 정보의 민감성 분류) 참조. 본 branch의 public path misconfiguration → INTERNAL_AUTH_MISCONFIGURATION 500 + P1 alert 분류 정책과 정합. JWT/secret 직접 source는 미발견 — follow-up 후보로 유지.
TODO
TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "AuthN/AuthZ Decision Matrix" / "Decisionized Work Items" 참조. missing/malformed/expired/invalid signature/issuer/audience/claim mapping/client message/CORS/token-PII log 모두 matrix row 또는 결정 라인으로 반영됨. 잔존 TODO 없음.
진행 중 메모
- security event log에는 principal 식별자를 최소화합니다.
결정 사항 (decisions)
- 2026-05-21: JWT Resource Server를 baseline security model로 둠.
- 2026-05-22: JWT key rotation/JWKS refresh failure는 explicit security failure catalog에 포함. unknown
kid, stale JWKS, refresh failure, rotation overlap window를 분리. - 2026-05-22: CORS는 allowlist default, credentials false default, preflight max-age 600s default. gateway override 시 mapping table 필요.
- 2026-05-22: API gateway/WAF/Ingress가 TLS/request-size/WAF/rate-limit을 선차단할 수 있으며, app envelope bypass 가능성을 runbook에 명시.
- 2026-05-22: JWT clock skew tolerance = 60s (Spring Security JwtTimestampValidator leeway). skew 초과 expired는 AUTH_TOKEN_EXPIRED.
- 2026-05-22: JWKS refresh interval = 10분, on-demand refresh on unknown kid (rate-limited 1회/1분).
- 2026-05-22: rotation overlap window = 새 kid 도입 → 24h 동안 old kid 병행 → cutover.
- 2026-05-22: CORS allowlist SSOT = app-level 우선, gateway/WAF는 보조. allowlist origin은 env-driven runtime configuration의
APP_SECURITY_CORS_ORIGINS로 주입. - 2026-05-22: public path misconfiguration 판정 알고리즘 =
SecurityFilterChain dump를 startup 시 snapshot→ 2026-06-09 as-built 정합:SECURITY_PUBLIC_PATHS(env, permitAll 의 결정론적 SSOT) 를 snapshot 으로 저장, 다음 build 와 diff (filter-chain reflection 은 Spring 버전 brittle → 폐기,build.gradle:185-188). public path 변경 시 snapshot 재생성+commit 요구. 한계: Java 하드코딩permitAll()(env 우회)은 미검출(§구현 가이드 5 참조). - 2026-05-22: secret rotation 책임 분담 = secrets-config-source-contract SSOT consume. 본 branch는 JWT signing key rotation의 운영 관측(JWKS refresh, kid mismatch 분류) 책임만 owns. secret 저장/주입은 secrets branch에 위임.
- 2026-05-22: JWT key rotation overlap(24h) ≥ idempotency TTL(24h)는 의도된 정합. idempotent replay가 key rotation cutover를 안전하게 가로지름. rate-limit-idempotency branch와 invariant.
Work Item Contract
각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 기준 작성으로 남아 있으면 이 branch는 완료로 보지 않습니다.
| field | required | rule |
|---|---|---|
| Decision | yes | 구현자가 선택해야 하는 기본값 |
| Allowed | yes | 허용되는 예외와 조건 |
| Forbidden | yes | 절대 금지되는 구현/문서 상태 |
| Required registry update | conditional | error/env/header/log/metric/capability 변경 시 필수 |
| Required contract test | yes | 계약 위반 시 실패해야 하는 테스트 |
| Failure condition | yes | review/build에서 실패로 판정할 상태 |
| Canonical extraction target | yes | wiki/projects 승급 위치 |
AuthN/AuthZ Decision Matrix
| 상황 | HTTP status | error.code | error.category |
|---|---|---|---|
| token 누락 | 401 | AUTH_TOKEN_MISSING | AUTH |
| token malformed (parse fail) | 401 | AUTH_TOKEN_MALFORMED | AUTH |
| token expired (clock skew tolerance 60s 초과) | 401 | AUTH_TOKEN_EXPIRED | AUTH |
| invalid signature | 401 | AUTH_TOKEN_INVALID_SIGNATURE | AUTH |
| issuer mismatch | 401 | AUTH_ISSUER_MISMATCH | AUTH |
| audience mismatch | 401 | AUTH_AUDIENCE_MISMATCH | AUTH |
| unknown kid (JWKS 미캐시) | 401 + Retry-After 5s | AUTH_KID_UNKNOWN | AUTH |
| JWKS endpoint outage (JWKS cache hit 시 통과, miss 시) | 401 (캐시 miss 후 fallback 실패) 또는 503 (JWKS outage 명확) | AUTH_JWKS_UNAVAILABLE | TRANSIENT_DEPENDENCY |
| claim mapping failure (subject/principal 추출 실패) | 401 | AUTH_CLAIM_MAPPING_FAILED | AUTH |
| valid token + 권한 부족 | 403 | AUTHZ_INSUFFICIENT_PERMISSION | AUTHZ |
| valid token + tenant cross-access (cross-tenant 시도) | 403 | AUTHZ_TENANT_MISMATCH | AUTHZ |
| public path misconfiguration (보호 endpoint가 unauthenticated 통과) | 500 + P1 alert | INTERNAL_AUTH_MISCONFIGURATION | INTERNAL |
Historical/superseded (pre-Phase-C2): 과거에는 production 분류가 coarse 3-code뿐이었다. Phase C2 기록은 12-code classifier·EntryPoint/DeniedHandler를
locally-verified로 보고하며 coarse 3-code는 non-filter fallback으로 유지한다고 한다. 현행 code owner 재확인은needs-confirmation이다.
Decisionized Work Items
| item | Decision | Allowed | Forbidden | Required test |
|---|---|---|---|---|
| JWT rotation | JWKS refresh + unknown kid + stale key classified |
cached key during overlap window | generic auth failure only | key rotation failure test |
| CORS | explicit origin allowlist, max-age 600s, credentials false | credentials true with exact origin only | wildcard with credentials | CORS preflight test |
| gateway/WAF | app documents bypassed envelope cases | gateway-owned 413/429 with correlation log | assuming all failures reach app | gateway mapping checklist |
| client message | generic auth/authz message | internal reason in secure log only | issuer/audience/token detail in response | leakage test |
결정-근거 매핑
각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. company-tech-blog 증거는
company-case-study로 표기하며 공식 best practice 로 승격하지 않음.
| Decision ID | Decision | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|
| D1 | JWT Resource Server 를 baseline security model 로 채택 (stateless 검증) | raw/official-docs/security-jwt-rfc-7519-validation.md#JWT-RFC7519-C1, raw/official-docs/security-jwt-rfc-7519-validation.md#JWT-RFC7519-C2, raw/official-docs/security-jwt-rfc-7519-validation.md#JWT-RFC7519-C4, raw/official-docs/security-authorization-cheatsheet-owasp.md#OWASP-AUTHZ-C3 |
official-standard + official-reference |
RFC 7519 는 claim 검증 spec 만 정의 — revocation / logout 메커니즘은 RFC 범위 밖, 별도 결정 필요 |
| D2 | clock skew tolerance = 60s (Spring JwtTimestampValidator leeway), 초과 expired → AUTH_TOKEN_EXPIRED |
raw/official-docs/security-jwt-rfc-7519-validation.md#JWT-RFC7519-C2, raw/official-docs/security-jwt-rfc-7519-validation.md#JWT-RFC7519-C3, raw/official-docs/security-spring-jwt-timestamp-validator-clock-skew.md#SS-JTVC-C1 |
official-standard (RFC "a few minutes" 상한) + official-vendor-doc (Spring default = 60s, SS-JTVC-C1) |
60s 가 운영 환경 NTP drift 에 충분한지 실증 필요; integration test (61s expired token reject) 미완료 |
| D3 | aud mismatch → 401 AUTH_AUDIENCE_MISMATCH 분류 |
raw/official-docs/security-jwt-rfc-7519-validation.md#JWT-RFC7519-C1 |
official-standard (MUST reject) |
다중 audience JWT 처리 시 식별 기준 선택 — RFC 범위 밖 |
| D4 | iss mismatch → 401 AUTH_ISSUER_MISMATCH 분류 |
raw/official-docs/security-jwt-rfc-7519-validation.md#JWT-RFC7519-C4 |
official-standard (application 재량으로 RFC 가 명시) |
401 vs 403 boundary case 선택은 RFC 가 강제하지 않음 — OWASP 권고 (OWASP-AUTHZ-C3) 으로 정당화 |
| D5 | deny-by-default + public path misconfiguration → 500 + P1 alert (SECURITY_PUBLIC_PATHS env snapshot diff — as-built; filter-chain reflection 폐기) |
raw/official-docs/security-authorization-cheatsheet-owasp.md#OWASP-AUTHZ-C1, raw/official-docs/security-authorization-cheatsheet-owasp.md#OWASP-AUTHZ-C2, raw/company-tech-blogs/security-toss-actuator-healthcheck.md#TOSS-HEALTH-C1 |
official-reference + company-case-study (OWASP cheat sheet 는 권고 — normative 표준 아님) |
env 기반이라 Java 하드코딩 permitAll()(env 우회) 미검출 (§구현 가이드 5 한계); snapshot diff false-positive |
| D6 | every-request 인증 검증 (stateless JWT 매 요청마다 검증) | raw/official-docs/security-authorization-cheatsheet-owasp.md#OWASP-AUTHZ-C5 |
official-reference |
session caching 미사용 시 verifier 부하 — JWKS cache + rate-limit (1회/1분) 으로 완화 |
| D7 | 401 (authn) vs 403 (authz) 분리, AUTHZ category 는 valid token + 권한/tenant 불일치에만 사용 | raw/official-docs/security-authorization-cheatsheet-owasp.md#OWASP-AUTHZ-C3, raw/official-docs/security-authorization-cheatsheet-owasp.md#OWASP-AUTHZ-C4, raw/official-docs/rfc9110-http-semantics.md#RFC9110-C23 (401 = 인증 자격 부재 + WWW-Authenticate MUST → AUTH matrix), raw/official-docs/rfc9110-http-semantics.md#RFC9110-C24 (403 = server 가 이해했으나 자격 불충분 → AUTHZ matrix) |
official-reference (OWASP authn/authz 분리) + official-standard (RFC 9110 §15.5.2/§15.5.4 가 401/403 HTTP semantics 정의) |
경계 case(valid token + scope vs role)에서 401 vs 403 선택은 RFC 가 강제 안 함 — application 결정. (RFC 7235 는 RFC 9110 이 obsolete — 9110 이 현행) |
| D8 | gateway/WAF + app envelope 이중 enforcement (defense in depth) | raw/official-docs/security-authorization-cheatsheet-owasp.md#OWASP-AUTHZ-C6 |
official-reference |
gateway bypass 시나리오 (직접 pod 접근 등) 의 envelope coverage 검증 필요 |
| D9 (2026-05-31 보강) | CORS allowlist default, credentials false default, max-age 600s, gateway override 시 mapping table | raw/official-docs/fetch-spec-cors.md#FETCH-CORS-C1 (CORS protocol = cross-origin 공유 여부 HTTP header 집합), #FETCH-CORS-C2 (preflight = OPTIONS + Access-Control-Request-Method), #FETCH-CORS-C3 (credentials mode include 시 Access-Control-Allow-Origin: * 금지 — normative), #FETCH-CORS-C4 (Access-Control-Allow-Credentials = credentials mode 응답 공유 제어), #FETCH-CORS-C5 (Access-Control-Max-Age 기본 5초, UA-imposed upper limit 별도). API branch cross-cite: raw/branch-notes/feature-api-contract-baseline D13 (OPTIONS preflight envelope 우회) |
official-standard (WHATWG Fetch — living standard, browser-side normative) |
wildcard * + credentials true 조합 금지의 1차 normative 근거는 FETCH-CORS-C3. max-age 600s 의 정확한 숫자 는 FETCH-CORS-C5 가 "5초 기본 + UA upper limit" 만 명시 — 600s 는 project-internal trade-off (UA cache hit 율 ↑ vs CORS rule 변경 propagation 지연). gateway/WAF override 시 mapping table 의무는 표준 외 (project-internal). 후속: Spring CorsConfiguration.checkOrigin() 의 startup 검증 동작은 별도 vendor doc 필요 (Claims To Verify 참조) |
| D10 | JWKS refresh interval = 10분, unknown kid on-demand refresh (rate-limited 1회/1분), rotation overlap window 24h |
raw/official-docs/jwks-nimbus-jose-jwksourcebuilder-spring-integration.md#NIMBUS-JWKS-C5 (Spring 기본 JWKS cache 5min — 10분은 그 2배, project trade-off), #NIMBUS-JWKS-C6 (unknown kid → JWKSetCacheRefreshEvaluator + cache.invalidate() on-demand refresh, Spring Security 6.x #11638 이후), #NIMBUS-JWKS-C4 (withJwkSetUri() 기본 rateLimited(false)+refreshAheadCache(false) → 1/min rate-limit 은 Nimbus JWKSourceBuilder 또는 app-layer 로 별도 구현), raw/official-docs/jwks-keycloak-key-rotation-active-passive.md#KC-ROT-C1 (Keycloak active/passive key = overlap 메커니즘의 IdP-side 근거), raw/company-tech-blogs/jwks-workos-unknown-kid-refresh-rate-limit-pattern.md#WORKOS-JWKS-C4 (overlap = token TTL + cache TTL + buffer 공식), #WORKOS-JWKS-C2 (rate-limit 5~10min 권고) + docs/runbooks/auth-token-rotation-failure.md (24h overlap 운영 절차) |
official-vendor-doc (메커니즘) + company-case-study (exact numbers — best practice 승격 금지) |
메커니즘은 지지, exact number 는 UNSUPPORTED_IMPL_DECISION: (1) 10min cache = Caffeine expireAfterWrite(10m) 로 표현 가능하나 숫자는 project trade-off. (2) 1/min rate-limit < WorkOS 권고 5~10min → thundering-herd/DoS 방어 약함(WORKOS-JWKS-C2 와 충돌 — 더 빠른 kid 전파를 위한 의도적 aggressive 선택). (3) 24h overlap 의 공식(token TTL+cache TTL+buffer) 정합은 Keycloak realm access-token TTL 확인 후 재평가 — ca-tmpl repo 엔 token TTL 부재(IdP-side, NEEDS_CONTEXT) |
| D11 | 한국 사례 토스 — health detail 의 보안 민감성 (보조 정합 참조) | raw/company-tech-blogs/security-toss-actuator-healthcheck.md#TOSS-HEALTH-C1 |
company-case-study (best practice 승격 금지) |
actuator security branch (raw/branch-notes/feature-management-actuator-security-contract) 와 cross-link 필요 — 본 security baseline 의 INTERNAL_AUTH_MISCONFIGURATION 정책과 정합성 확인 |
구현 가이드
결정 (Decisions) 이 "무엇 을 할 것인가" 라면, 본 §는 "어디에 어떻게 구현될 것인가" 의 사전 명세. ca-tmpl
src/실제 클래스/패키지/registry 값을 anchor 로 쓰되, 코드로 확인된 것은actually-implemented, registry/설계만 있고 코드 미확인은planned로 표기한다 (2026-06-08src/grep 검증).3-rule meta principle (CLAUDE.md §15.5): R1 모든 cell 은 Decision ID + Supporting Claim reference / R2 근거 없는 detail 은
UNSUPPORTED_IMPL_DECISION+ trade-off / R3 본 branch 범위 밖 detail 은 §Audit & Findings 로 이관.
1. SecurityFilterChain wiring — deny-by-default + stateless
Trace: D1 (JWT Resource Server) · D5 (deny-by-default,
OWASP-AUTHZ-C1/C2) · D6 (every-request,OWASP-AUTHZ-C5).
- UNSUPPORTED_IMPL_DECISION: CSRF disable 결정 — OWASP 는 stateless+비쿠키 시 CSRF 무관함을 함의하나 명시 권고는 아님. trade-off: JWT in
Authorizationheader(쿠키 아님) → CSRF 표면 없음 → disable 로 필터 단순화.
| 항목 | 구현 anchor | 등급 |
|---|---|---|
| 필터체인 Bean | dev.caskeleton.adapter.web.auth.SecurityConfig#filterChain (src/adapter-web/.../auth/SecurityConfig.java) |
actually-implemented |
| deny-by-default | auth.requestMatchers(publicPaths).permitAll() → auth.anyRequest().authenticated() |
actually-implemented |
| stateless | sessionManagement(STATELESS) |
actually-implemented |
| CSRF off | csrf(csrf -> csrf.disable()) |
actually-implemented |
| resource server | oauth2ResourceServer(oauth -> oauth.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtConverter))) |
actually-implemented |
| public paths source | SecuritySettings#publicPaths() ← env SECURITY_PUBLIC_PATHS (application.yml L171 ca-skeleton.security.public-paths) |
actually-implemented |
| Cache-Control writer | headers(h -> h.cacheControl(c -> c.disable())) — 단일 owner 위임: raw/branch-notes/feature-api-contract-baseline D16 CacheControlFilter 가 Cache-Control: no-store + Vary 발행 |
actually-implemented (cross-contract) |
2. JWT validation chain — current-known + code recheck gate
Trace: D2 · D3 · D4. pre-C2 auto-config-only 설명은 historical/superseded다. Phase C2 기록은
SupplierJwtDecoder기반 custom bean과 explicit 60s validator chain을 보고하지만, 현재 workspace에 code owner가 없어 현행 여부는needs-confirmation이다.
- IMPL trade-off (근거 확보됨): clock skew 60s 는 Spring default leeway 이며 그 default 값이 60s 임은
SS-JTVC-C1(official-vendor-doc, "Resource Server configures a clock skew of 60 seconds")로 확정. 단 코드는.clockSkew(Duration.ofSeconds(60))를 명시 설정하지 않고 default 에 의존 → Spring version 이 default 를 바꾸면 silent drift. trade-off: 명시 설정(drift 차단, 코드 1줄) vs default 의존(설정 최소화). §Claims To Verify 의 integration test(61s reject)로 잔여 검증.
| 항목 | 구현 anchor | 등급 |
|---|---|---|
| JWT decoder | Phase C2 report: JwtDecoderConfig의 lazy SupplierJwtDecoder custom bean. pre-C2 auto-config-only 경로는 superseded |
locally-verified(2026-06-08 기록) / current code needs-confirmation |
| issuer·audience 검증 | Phase C2 report: issuer + audience validator chain | locally-verified(보고) / current code needs-confirmation |
| expiry/clock skew (D2) | vendor default 60s 근거는 유효. Phase C2 report는 explicit 60s와 30s/90s boundary test를 기록 | locally-verified(보고) / current code needs-confirmation |
| settings binding | dev.caskeleton.adapter.web.settings.SecuritySettings — issuerUri required fail-fast, audience 누락 시 warn+skip, publicPaths |
actually-implemented |
claim→principal mapping (matrix AUTH_CLAIM_MAPPING_FAILED) |
dev.caskeleton.adapter.web.auth.JwtToAuthenticatedUserConverter — sub→principal, realm_access+resource_access roles → ROLE_* |
actually-implemented (단 실패 시 전용 code 매핑은 §3 drift) |
3. Auth 실패 → error code 분류 (matrix 집행)
Trace: AuthN/AuthZ Decision Matrix 12행 · D7 (401/403 분리,
OWASP-AUTHZ-C3/C4). registry SSOT =docs/registries/error-codes.yaml(owner_branch = 본 branch, 12 codes).
- CODE_GRANULARITY_DRIFT (§Audit & Findings): 설계는 12 codes, production enum 은 3 codes. 아래 표는 현재 코드 실체 와 설계 계약 을 분리 표기.
| 분류 단계 | 구현 anchor | 등급 |
|---|---|---|
| auth 예외 핸들러 | dev.caskeleton.adapter.web.error.GlobalExceptionHandler L81–94 (@ExceptionHandler × 3) |
actually-implemented |
InvalidBearerTokenException → OperationalError.INVALID_TOKEN (AUTH 401) |
GlobalExceptionHandler#handleInvalidToken |
actually-implemented (coarse) |
AuthenticationException → OperationalError.UNAUTHENTICATED (AUTH 401) |
GlobalExceptionHandler#handleUnauthenticated |
actually-implemented (coarse) |
AccessDeniedException → OperationalError.FORBIDDEN (AUTHZ 403) |
GlobalExceptionHandler#handleForbidden |
actually-implemented (coarse) |
| fine-grained 12 codes | Phase C2 report: OperationalError + registry mapping test에 구현, coarse 3-code는 fallback 유지 |
locally-verified(보고) / current code needs-confirmation |
| fine-grained 분류 메커니즘 | Phase C2 report: SecurityErrorClassifier + EnvelopeAuthenticationEntryPoint/EnvelopeAccessDeniedHandler가 filter-layer 오류를 분류 |
locally-verified(보고) / current code needs-confirmation |
4. JWKS rotation & unknown-kid 운영 정책 (D10)
Trace: D10 (JWKS 10min refresh / on-demand unknown-kid / 24h overlap), Supporting
NIMBUS-JWKS-C4/C5/C6·KC-ROT-C1·WORKOS-JWKS-C2/C4. 본 branch 는 secret 저장/주입 이 아니라 JWT signing key rotation 의 운영 관측만 owns (2026-05-22 결정; 저장은 raw/branch-notes/feature-secrets-config-source-contract 위임).
- UNSUPPORTED_IMPL_DECISION (자동조사 2026-06-08 완료 후 정제): 메커니즘은 vendor doc 으로 지지되나 exact number 는 project trade-off. (1) 10min = Spring 기본 5min(
NIMBUS-JWKS-C5)의 2배 → CaffeineexpireAfterWrite(10m). (2) 1/min < WorkOS 권고 5~10min(WORKOS-JWKS-C2) — 더 빠른 kid 전파 vs thundering-herd/DoS 방어 약화의 의도적 aggressive 선택. (3) 24h overlap =WORKOS-JWKS-C4공식(token TTL+cache TTL+buffer) — Keycloak token TTL 확인 후 재평가(NEEDS_CONTEXT).
| 항목 | 구현 anchor | 등급 |
|---|---|---|
| JWKS 자동 resolve | issuer-uri /.well-known/openid-configuration → Nimbus JWKS auto-discovery (auto-config). 기본 cache TTL 5min, rateLimited(false)+refreshAheadCache(false) (NIMBUS-JWKS-C4/C5) |
actually-implemented (Nimbus default cache) |
| unknown kid on-demand refresh | Spring Security 6.x(#11638 이후)가 unknown kid 감지 시 JWKSetCacheRefreshEvaluator → cache.invalidate() → 재조회 (NIMBUS-JWKS-C6). 단 rate-limit 없음 — Spring layer 미제공 |
actually-implemented (refresh) / rate-limit planned |
| 10min cache + 1/min rate-limit (메커니즘 선택지) | 택1: (A) NimbusJwtDecoder.withJwkSetUri(...).cache(caffeine expireAfterWrite(10m)) + app-layer rate-limit(Bucket4j) — auto-config 유지; (B) withJwkSource(JWKSourceBuilder.create(uri).refreshAheadCache(...).rateLimited(60_000)) — Nimbus built-in(NIMBUS-JWKS-C2/C3), auto-config override 필요. 둘 다 미작성 |
planned |
| 24h rotation overlap | IdP-side: Keycloak active/passive key(KC-ROT-C1). 운영 절차 documented: docs/runbooks/auth-token-rotation-failure.md §4 ("publish → 24h 대기 → switch", 비상 시 cache TTL 60s 강제, overlap 48h 일시 확장) |
documented-only (runbook + IdP 설정) |
| 분류 code | AUTH_KID_UNKNOWN(retryable=true, Retry-After 5s) · AUTH_JWKS_UNAVAILABLE(TRANSIENT_DEPENDENCY) registry 등록 |
documented-only (§3 drift 적용 — 미구현) |
5. public path misconfiguration guard (D5)
Trace: D5 (
OWASP-AUTHZ-C1/C2deny-by-default) + §테스트 계약 snapshot diff.
- 2026-06-09 정합 (as-built 메커니즘 변경): 노트 초안은 "startup 시
SecurityFilterChain.getFilters()introspection 으로 snapshot" 을 명세했으나, as-built 게이트는SecurityFilterChainreflection 을 쓰지 않는다.src/build.gradle:185-188가 명시적으로 그 결정을 기록: filter-chain reflection 은 Spring 버전 간 brittle → 대신permitAll()을 실제로 먹이는 결정론적 SSOT 인SECURITY_PUBLIC_PATHS(src/.env →SecuritySettings.publicPaths()) 를 snapshot. 즉verifyPublicPathSnapshot은 env 의 public-path 목록을docs/security/public-paths-snapshot.txt와 diff.- ⚠️ 한계(정직 고지): env 기반이므로 Java 코드에 하드코딩된
permitAll()(SECURITY_PUBLIC_PATHS우회)은 이 게이트가 못 잡는다. "보호 endpoint 의 silent 노출 차단" 보장은 모든 public path 가 env 를 경유 한다는 전제에서만 성립. (filter-chain 실측 introspection 으로 승급하려면 brittle-reflection trade-off 재검토 필요.)- UNSUPPORTED_IMPL_DECISION: snapshot-diff 메커니즘 자체 — OWASP 는 deny-by-default 원칙 만 권고. trade-off: 정상 PR 의 path 추가마다 review(false-positive) vs unintended public path 통과 차단.
| 항목 | 구현 anchor | 등급 |
|---|---|---|
| snapshot 추출 | src/build.gradle:197~ — SECURITY_PUBLIC_PATHS(src/.env) 파싱 → docs/security/public-paths-snapshot.txt (filter-chain reflection 아님, build.gradle:185-188 결정) |
actually-implemented |
| diff gate | gradle task (build.gradle public-path snapshot 검증; 변경 시 snapshot 재생성+commit 요구) |
actually-implemented |
| 위반 분류 | INTERNAL_AUTH_MISCONFIGURATION (INTERNAL 500 + P1 alert) |
documented-only (enum/registry 등록, runtime emit 코드 부재) |
6. CORS 정책 (D9)
Trace: D9 (
FETCH-CORS-C3wildcard+credentials 금지 normative,FETCH-CORS-C5max-age). API branch cross-cite: raw/branch-notes/feature-api-contract-baseline D13 (OPTIONS preflight envelope 우회).
- UNSUPPORTED_IMPL_DECISION: max-age 600s — FETCH-CORS-C5 는 "기본 5초 + UA upper limit" 만. trade-off: UA preflight cache hit ↑ vs CORS rule 변경 propagation 지연 ↑.
| 항목 | 구현 anchor | 등급 |
|---|---|---|
| CORS source | SecurityConfig#corsConfigurationSource + UrlBasedCorsConfigurationSource("/**") |
actually-implemented |
| settings | dev.caskeleton.adapter.web.settings.CorsSettings (record, @Validated, prefix ca-skeleton.cors) |
actually-implemented |
| enabled toggle | CorsSettings#enabled ← APP_SECURITY_CORS_ENABLED; disabled → 빈 source (CORS inactive) |
actually-implemented |
| origins (D9 allowlist) | allowedOrigins ← APP_SECURITY_CORS_ORIGINS; enabled+empty → fail-fast throw (cross-field, JSR-303 불가) |
actually-implemented |
| methods | default [GET,POST,PATCH,PUT,DELETE,OPTIONS] ← APP_SECURITY_CORS_ALLOWED_METHODS |
actually-implemented |
| headers | default ["*"] ← APP_SECURITY_CORS_ALLOWED_HEADERS |
actually-implemented |
| credentials (D9 false default) | allowCredentials ← APP_SECURITY_CORS_ALLOW_CREDENTIALS |
actually-implemented |
| max-age 600s | maxAgeSeconds ← APP_SECURITY_CORS_MAX_AGE, @PositiveOrZero |
actually-implemented (숫자는 env-driven; 600s 는 §UNSUPPORTED 위) |
| wildcard+credentials 정적 거부 (D9 normative) | Phase C2 report: CorsSettings가 enabled+["*"]+credentials=true를 startup fail-fast |
locally-verified(보고) / current code needs-confirmation |
7. PII 로그 redaction
Trace: §진행 중 메모("principal 식별자 최소화") + §테스트 계약("token in log = fail").
- UNSUPPORTED_IMPL_DECISION: redaction 메커니즘 미정 — log masking 강제는 raw/branch-notes/feature-secrets-config-source-contract/log-management 계약과 겹침. trade-off: 본 branch 는 contract test(grep
eyJ/Bearer)로 위반 검출만 owns, masking filter 구현은 위임.
| 항목 | 구현 anchor | 등급 |
|---|---|---|
| token leak contract test | Phase C2 report: entry-point 응답·로그에서 Authorization/Bearer/JWT(eyJ) 노출을 거부하는 contract test |
locally-verified(보고) / current code needs-confirmation |
| principal 최소화 | security event log 에 principal 식별자 최소화 | documented-only |
엣지·실패·의존
R4(깊이 게이트) 캡처용. 정상 경로 외에 구현 중 부딪힐 실패/엣지/다른 계약 의존.
- 실패·엣지 경로:
- JWKS endpoint outage: cache hit 시 통과, miss 시
AUTH_JWKS_UNAVAILABLE(TRANSIENT_DEPENDENCY) — outage 명확하면 503, cache miss 후 fallback 실패면 401. runbookauth-token-rotation-failure.md§2 (cache TTL 60s 강제) 발동. - unknown
kid(rotation 직후): on-demand refresh(rate-limited) → 여전히 미해결이면AUTH_KID_UNKNOWN(retryable=true, Retry-After 5s). 24h overlap window 내면 old kid 로 검증 통과. - clock skew 경계: 60s leeway 초과 expired만
AUTH_TOKEN_EXPIRED. NTP drift > 60s 면 정상 token 도 오판 → NTP sync 운영 의존. - public path 오설정: Phase C2 report의 env snapshot gate가 drift를 차단한다. 단 Java hard-coded
permitAll()은 미검출이며 current task 존재는 재확인 필요. - CORS wildcard+credentials: Phase C2 report는 startup fail-fast를 기록한다. current code 재확인 전까지
needs-confirmation. - 다중 audience JWT:
aud가 list 일 때 식별 기준 미정의 (RFC 범위 밖, Open Risk D3).
- JWKS endpoint outage: cache hit 시 통과, miss 시
- 다른 계약 의존:
- raw/branch-notes/feature-operational-error-observability-foundation
D10—Category10-value enum SSOT (shared/error/Category.java: AUTH/AUTHZ/TRANSIENT_DEPENDENCY/INTERNAL 등). 본 branch 의 모든error.category가 이 enum 을 consume. enum 변경 시 matrix 영향. - raw/branch-notes/feature-api-contract-baseline
D16—CacheControlFilter가 Cache-Control 단일 owner. 본 branch 는 Spring Security 의 default cache writer 를 disable 하여 충돌 회피.D13— OPTIONS preflight envelope 우회(CORS D9 와 정합).D8— request body size 413(보안 baseline 의 upload 와 인접). - raw/branch-notes/feature-secrets-config-source-contract — JWT signing key 저장/주입/rotation script. 본 branch 는 rotation 의 운영 관측만 owns. secret source 계약 변경 시 JWKS resolver 입력 영향.
- raw/branch-notes/feature-rate-limit-idempotency-contract — idempotency TTL 24h ≥ key rotation overlap 24h invariant. replay 가 rotation cutover 를 안전 통과해야 함(교차 시나리오 테스트 cross-link).
- raw/branch-notes/feature-management-actuator-security-contract — actuator(제어면) 보안. 본 branch(데이터면)의
INTERNAL_AUTH_MISCONFIGURATION와 health detail 노출 정책(D11) 정합. - raw/branch-notes/feature-env-driven-runtime-configuration
D10—@ConfigurationProperties+ JSR-303 + fail-fast 검증 패턴.CorsSettings/SecuritySettings가 이 패턴을 따름.
- raw/branch-notes/feature-operational-error-observability-foundation
테스트 계약
- token 값이 log에 나오면 실패.
- expired token과 invalid signature가 같은 internal code로 뭉개지면 실패.
- public path snapshot diff 검사:
SECURITY_PUBLIC_PATHSenv SSOT를docs/security/public-paths-snapshot.txt와 비교하는./gradlew verifyPublicPathSnapshot을 사용하고, 의도한 변경은-PapprovePublicPathChange승인 경로로 처리한다. reflection은 폐기됐으며 Java hard-codedpermitAll()은 이 gate가 탐지하지 못한다. Phase C2 report의 task 존재·CI wiring은 current code owner에서 재확인한다. - client response에 issuer/audience 내부 값이 과노출되면 실패.
- unknown
kid/JWKS refresh failure/key rotation overlap이 분류되지 않으면 실패. - wildcard CORS + credentials 허용이면 실패.
검증해야 할 주장
공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
Spring JwtTimestampValidator 의 default leeway 가 60s 와 일치 |
SS-JTVC-C1 "By default, Resource Server configures a clock skew of 60 seconds." (official-vendor-doc). 코드상 .clockSkew() 명시 설정 없음(default 의존) — Spring version drift 위험은 유지 |
통합 테스트로 61s expired token 거절 확인 (auto-config 경로 검증) | needs-implementation-test |
| fine-grained 12 codes의 현행 production emit | Phase C2 report는 구현·테스트 완료를 기록하지만 current code owner가 이 workspace에 없음 | 현행 OperationalError, classifier, EntryPoint/DeniedHandler와 expired/signature/issuer 분리 test를 재실행 |
needs-confirmation (reported locally-verified) |
| env public-path snapshot gate의 현행 task·CI wiring | Phase C2 report는 verifyPublicPathSnapshot 구현을 기록하지만 current code 미확인 |
task 목록 확인 후 env path 변경→fail, approval flag→pass를 재실행. hard-coded permitAll() blind spot 별도 기록 |
needs-confirmation (reported locally-verified) |
| CORS wildcard + credentials true startup 거부 | Phase C2 report는 CorsSettings fail-fast 구현을 기록하지만 current code 미확인 |
현행 settings test에서 enabled+wildcard+credentials=true startup failure 확인 | needs-confirmation (reported locally-verified) |
JWKS refresh 10min: Caffeine expireAfterWrite(10m) + withJwkSetUri().cache() 조합으로 표현 |
메커니즘은 NIMBUS-JWKS-C5 로 지지(Spring 기본 5min, Cache 주입 가능). 10min exact value 는 project trade-off |
Caffeine + Spring Cache 통합 integration test: JWKS endpoint mock → 10분 후 fetch 재트리거 확인 | needs-implementation-test |
| unknown kid on-demand refresh 동작 + 1/min rate-limit | refresh 자체는 NIMBUS-JWKS-C6(Spring 6.x #11638) 로 지지. rate-limit 은 Spring layer 미제공(NIMBUS-JWKS-C4) — Nimbus JWKSourceBuilder.rateLimited()(Alt B) 또는 app-layer Bucket4j(Alt A) 설계 결정 필요. 1/min < WorkOS 5~10min(WORKOS-JWKS-C2) |
Alt A/B 중 택1 후 JWKS endpoint mock + unknown kid 연속 요청으로 rate-limit 측정 | needs-design-decision |
rotation overlap 24h 가 WORKOS-JWKS-C4 공식(token TTL+cache TTL+10min)과 정합 |
ca-tmpl repo 엔 access-token TTL 부재(Keycloak realm-side, IdP 설정). TTL < (24h−cache−buffer) 여야 공식 충족 | ca-tmpl Keycloak realm client access-token lifespan 확인 후 24h 재평가 | needs-confirmation (NEEDS_CONTEXT: token TTL) |
| rotation overlap window 24h 가 idempotency TTL 24h 와 안전하게 정합 | invariant 가정은 별도 raw source 미증명 — replay+rotation 교차 시나리오 테스트 필요 | raw/branch-notes/feature-rate-limit-idempotency-contract 의 contract test 와 cross-link, key rotation mid-replay 시나리오 통합 테스트 작성 | planned |
| token 값이 log에 등장하지 않음 | Phase C2 report는 redaction contract test를 기록하지만 current code/log configuration 미확인 | 현행 test와 log output을 대상으로 Authorization/Bearer/eyJ self-grep 재실행 |
needs-confirmation (reported locally-verified) |
INTERNAL_AUTH_MISCONFIGURATION 500 + P1 alert 가 prod runbook 에 등록 |
runbook auth-token-rotation-failure.md 는 stub 단계. alert routing / paging 정책 별도 확인 필요 |
raw/branch-notes/feature-runtime-health-lifecycle-contract 또는 metric-alerting branch 와 cross-link, alertmanager rule 추가 PR | planned |
관심사 커버리지 (coverage-auditor 자동 생성 — 있을 때)
governing doc = wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets 의 축 1: 데이터면 인증/인가(§프로젝트 컨텍스트 1번).
/coverage가 재생성하는 초안 — 손유지 금지. 기준:rules/coverage-gate.md. 축 2(Actuator)·축 3(Secrets)는 본 branch 밖 → delegated.
| 관심사 (governing doc 축 1) | 상태 | owner | 심각도 | 근거 |
|---|---|---|---|---|
| JWT Resource Server (deny-by-default authn) | covered-here | — | — | D1, D5, D6 / §구현가이드 1 |
| AuthN/AuthZ matrix 12행 (분류 계약) | covered-here | — | — | §AuthN/AuthZ Matrix, D3/D4/D7 / §구현가이드 3 |
| clock skew tolerance 60s | covered-here | — | — | D2 / §구현가이드 2 |
| JWKS 10분 refresh + unknown kid | covered-here | — | — | D10 / §구현가이드 4 |
| key rotation overlap 24h | covered-here | — | — | D10 / runbook auth-token-rotation-failure.md |
| public path snapshot diff | covered-here | — | — | D5 / §구현가이드 5 / §테스트 계약 |
| CORS allowlist + credentials false + max-age | covered-here | — | — | D9 / §구현가이드 6 |
| 401/403 분리 (authn vs authz) | covered-here | — | — | D7 / §구현가이드 3 |
| token/PII 로그 금지 | covered-here | — | — | §구현가이드 7 / §테스트 계약 |
| JWT signing key 저장/주입/rotation script | delegated | raw/branch-notes/feature-secrets-config-source-contract | OK | raw/branch-notes/feature-secrets-config-source-contract (2026-05-22 결정 / registry owner_branch) |
| Actuator 제어면 보안 (port 9001 + allowlist) | delegated | raw/branch-notes/feature-management-actuator-security-contract | OK | raw/branch-notes/feature-management-actuator-security-contract (governing doc 축 2 / D11 cross-link) |
Category enum SSOT (error.category) |
delegated | raw/branch-notes/feature-operational-error-observability-foundation | OK | raw/branch-notes/feature-operational-error-observability-foundation (shared/error/Category.java / §엣지·실패·의존) |
마주친 문제
- 아직 없음.
Audit & Findings (2026-06-08 — /branch-spec ca-tmpl ground-truth 대조)
src/코드·docs/registries·runbook 을 읽고 노트의 self-report 와 대조한 결과. 사용자 작성 결정 영역은 자동 rewrite 하지 않고 정합 권고만 남긴다 (CLAUDE.md §11,/branch-spec§2).
Phase C2 구현 완료 (2026-06-08, locally-verified)
사용자 지시 "문서 보고 하나도 빠짐없이 구현" 당시의 보존 기록. JWKS cache/rate-limit만 Minimal 결정으로 제외됐고 당시 test suite GREEN으로 기록됐다. 현재 workspace에는 code owner가 없어 이 표는 당시 evidence grade를 보존하되 현행 상태 증명으로 재사용하지 않는다.
| 구현 항목 | 파일 | 등급 | 비고 |
|---|---|---|---|
| 12 fine-grained AUTH/AUTHZ/INTERNAL codes | shared-contract/.../OperationalError.java |
locally-verified |
registry SSOT 와 status/category/retryable 일치. ErrorCodeRegistryMappingTest·BusinessRuleValidationContractTest GREEN. coarse 3-code 는 non-filter fallback 으로 유지 |
| exception → fine-grained 분류기 | adapter-web/.../auth/SecurityErrorClassifier.java |
locally-verified |
JwtValidationException(exp/iss/aud) + BadJwtException(signature/malformed/kid) + JWKS outage 메시지 heuristic. 12개 unit test |
| AuthenticationEntryPoint / AccessDeniedHandler | adapter-web/.../auth/EnvelopeAuthenticationEntryPoint.java·EnvelopeAccessDeniedHandler.java·AuthErrorResponseWriter.java |
locally-verified |
filter-layer 실패를 Envelope 로 변환(=@RestControllerAdvice 미도달 문제 해소). WWW-Authenticate(401 MUST, RFC9110-C23) + Retry-After(KID 5s/JWKS 30s) |
| token/PII redaction | (entry point) | locally-verified |
응답·로그에 eyJ/Bearer/raw message 미노출 — token_value_never_leaks... contract test |
| explicit clock skew 60s + issuer + audience | adapter-web/.../auth/JwtDecoderConfig.java |
locally-verified |
custom JwtDecoder bean(SupplierJwtDecoder lazy → startup 시 IdP 불필요). validator chain unit test(30s 통과 / 90s 거절 = D2 silent-drift 위험 해소) |
| CORS wildcard+credentials 정적 거부 | adapter-web/.../settings/CorsSettings.java |
locally-verified |
enabled+["*"]+credentials=true → startup fail-fast (D9/FETCH-CORS-C3). Spring runtime 의존 제거 |
| public path snapshot gate | src/build.gradle verifyPublicPathSnapshot + docs/security/public-paths-snapshot.txt |
locally-verified |
drift → build fail, -PapprovePublicPathChange 로 승인. fail/approval path 수동 검증 완료 |
| JWKS 10min cache + 1/min rate-limit | — | documented-only |
Minimal 결정: exact number 는 NEEDS_CONTEXT(Keycloak token TTL, IdP-side). Nimbus/Spring default cache 유지 |
| 24h rotation overlap | runbook + IdP | documented-only |
IdP-side(Keycloak active/passive), 변경 없음 |
구현 중 발견(드리프트 정정): OperationalErrorTest.internal_category_codes_are_retryable 가 "모든 INTERNAL = retryable" 를 단언했으나 registry 는 INTERNAL_AUTH_MISCONFIGURATION 을 retryable=false 로 둠(redeploy 필요한 deterministic config bug). registry SSOT 가 옳다고 판단 → enum 을 false 로 맞추고 테스트에 misconfig 예외를 명시. → raw/errors/internal-auth-misconfiguration-retryable-invariant-conflict-2026-06-08
CODE_GRANULARITY_DRIFT(설계 12 codes ↔ 코드 3 codes→ RESOLVED 2026-06-08, 옵션 (a)):docs/registries/error-codes.yaml의 12 fine-grained AUTH/AUTHZ/INTERNAL codes 를 productionOperationalErrorenum 에 추가하고, customEnvelopeAuthenticationEntryPoint+EnvelopeAccessDeniedHandler+SecurityErrorClassifier가JwtValidationException/BadJwtException/OAuth2Error를 inspect → expired/malformed/signature/issuer/audience/kid/jwks 로 분기 emit. coarse 3-code(UNAUTHENTICATED/INVALID_TOKEN/FORBIDDEN)는 controller 직접-throw 등 non-filter 경로 fallback 으로 유지. 등급: matrix·12codes =locally-verified. (옵션 (b) registry downgrade 는 미채택.)AUTH_KID_UNKNOWNretryable 정합 (drift 아님, 기록용): registry L124 가 2026-06-01retryable: false→true로 변경(JWKS 회전 중 ~5s 후 해소 가능, Retry-After 5s 와 정합). 노트 matrix 는 retryable 열이 없어 무영향. BusinessRuleValidationContractTest 가 이 retryable 값을 검증.- Historical/superseded — D2 default-only 설명: Phase C2 report는 explicit 60s custom decoder와 boundary test로 해소했다고 기록한다. current code owner 재확인 전에는 reported state와
needs-confirmation을 함께 유지한다. - Historical/superseded — snapshot/redaction 미구현 설명: Phase C2 report는 env snapshot gate와 token redaction contract test를 구현했다고 기록한다. JWKS exact cache/rate-limit만
documented-only로 남는다. current code는 별도 재확인 필요.
묶음
- raw/company-tech-blogs/jwks-workos-unknown-kid-refresh-rate-limit-pattern
- raw/company-tech-blogs/security-toss-actuator-healthcheck
- raw/official-docs/actuator-istio-sidecar-management-alt
- raw/official-docs/fetch-spec-cors
- raw/official-docs/jwks-keycloak-key-rotation-active-passive
- raw/official-docs/jwks-nimbus-jose-jwksourcebuilder-spring-integration
- raw/official-docs/owasp-file-upload-cheat-sheet
- raw/official-docs/rfc9110-http-semantics
- raw/official-docs/secrets-aws-secrets-manager-rotation
- raw/official-docs/security-authorization-cheatsheet-owasp
- raw/official-docs/security-aws-sigv4-hmac-signing
- raw/official-docs/security-jwt-rfc-7519-validation
- raw/official-docs/security-mtls-rfc-8705
- raw/official-docs/security-oauth2-pkce-rfc-8252
- raw/official-docs/security-opa-policy-engine-official
- raw/official-docs/security-spring-jwt-timestamp-validator-clock-skew
본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.
근거 자료
- raw/official-docs/fetch-spec-cors — WHATWG Fetch §3.3 CORS protocol: D9 wildcard+credentials 금지(FETCH-CORS-C3) + Access-Control-Max-Age 기본 5초(FETCH-CORS-C5) normative 근거. D9 의 UNSUPPORTED_DECISION 해소
- raw/official-docs/jwks-nimbus-jose-jwksourcebuilder-spring-integration — D10 JWKS refresh 메커니즘 (Spring/Nimbus cache·rate-limit·unknown-kid). 2026-06-08
wiki-decision-researcher자동조사 산출 - raw/official-docs/jwks-keycloak-key-rotation-active-passive — D10 rotation overlap IdP-side (Keycloak active/passive key). 2026-06-08 자동조사 산출
- raw/company-tech-blogs/jwks-workos-unknown-kid-refresh-rate-limit-pattern — D10 unknown-kid rate-limit + overlap 공식 (engineering practice). 2026-06-08 자동조사 산출
- raw/official-docs/security-spring-jwt-timestamp-validator-clock-skew — D2 clock skew 60s: Spring Security
JwtTimestampValidatordefault leeway = 60s (SS-JTVC-C1,official-vendor-doc). 2026-06-08wiki-source-summarizer산출 - raw/official-docs/rfc9110-http-semantics — D7 401/403 HTTP semantics (RFC9110-C23/C24). 기존 RFC 9110 raw 에 §15.5.2/§15.5.4 발췌 보강. 2026-06-08
오류 기록 (본 feature 작업 중 발생)
- raw/errors/internal-auth-misconfiguration-retryable-invariant-conflict-2026-06-08 — enum "모든 INTERNAL=retryable" 단언 ↔ registry
INTERNAL_AUTH_MISCONFIGURATIONretryable=false 충돌. SSOT(registry) 기준으로 정정 + 테스트에 예외 명시.
면접 준비 (이 작업에서 나올 수 있는 면접 질문)
- raw/interviews/jwt-resource-server-fine-grained-error-classification-2026-06-08 — resource-server 인증 실패를 401/403 으로만 뭉개지 않고 fine-grained 분류한 방법 (filter-layer 가
@RestControllerAdvice미도달 → custom EntryPoint, exception heuristic, token redaction).
Blog topics
- raw/blog-topics/spring-security-filter-layer-error-envelope-2026-06-08 — "Spring Security 인증 실패는 왜 @RestControllerAdvice 로 안 잡히나" + AuthenticationEntryPoint 로 공통 에러 Envelope 통일 + SupplierJwtDecoder lazy clock-skew 패턴.
관련 일일 노트
이 브랜치를 작업한 날짜들. 양방향 nav 유지.
- (없음 — Phase C2 실 구현 단계에서 누적)
완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- wiki 추출 대상 (verified만,
wiki/projects/로만 추출):actually-implemented항목: SecurityFilterChain(deny-by-default/stateless/CSRF off/resource server)·CorsSettings·SecuritySettings·JwtToAuthenticatedUserConverter (이전 단계)locally-verified항목 (2026-06-08): 12 fine-grained codes / SecurityErrorClassifier / EnvelopeAuthenticationEntryPoint·AccessDeniedHandler / JwtDecoderConfig(60s skew) / CORS wildcard+credentials 거부 /verifyPublicPathSnapshotgate / token redaction contract testprod-verified항목: (없음 — 실 IdP 연동 통합 테스트 미수행)
- 추출하지 않을 항목 (planned / documented-only / abandoned): JWKS 10min cache + 1/min rate-limit (Minimal 결정, exact number NEEDS_CONTEXT) / 24h rotation overlap (IdP-side) / 실 토큰 서명 통합 테스트(IdP 필요)