Files
llm-wiki/raw/branch-notes/feature-security-operational-baseline.md
T

53 KiB
Raw Blame History

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
ca-skeleton
wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets.md
branch
ca-skeleton
security
jwt
authentication
authorization
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
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1
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 기준의 인증/인가 실패 운영 분류를 정의합니다.

부모 (필수)

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 510min 권고 + overlap 공식(token TTL+cache TTL+10min) (WORKOS-JWKS-C1C4) — company-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.

후속 보강 (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 시 snapshot2026-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 includeAccess-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-08 src/ 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 Authorization header(쿠키 아님) → 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 CacheControlFilterCache-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.SecuritySettingsissuerUri required fail-fast, audience 누락 시 warn+skip, publicPaths actually-implemented
claim→principal mapping (matrix AUTH_CLAIM_MAPPING_FAILED) dev.caskeleton.adapter.web.auth.JwtToAuthenticatedUserConvertersub→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 L8194 (@ExceptionHandler × 3) actually-implemented
InvalidBearerTokenExceptionOperationalError.INVALID_TOKEN (AUTH 401) GlobalExceptionHandler#handleInvalidToken actually-implemented (coarse)
AuthenticationExceptionOperationalError.UNAUTHENTICATED (AUTH 401) GlobalExceptionHandler#handleUnauthenticated actually-implemented (coarse)
AccessDeniedExceptionOperationalError.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배 → Caffeine expireAfterWrite(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 감지 시 JWKSetCacheRefreshEvaluatorcache.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/C2 deny-by-default) + §테스트 계약 snapshot diff.

  • 2026-06-09 정합 (as-built 메커니즘 변경): 노트 초안은 "startup 시 SecurityFilterChain.getFilters() introspection 으로 snapshot" 을 명세했으나, as-built 게이트는 SecurityFilterChain reflection 을 쓰지 않는다. 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-C3 wildcard+credentials 금지 normative, FETCH-CORS-C5 max-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#enabledAPP_SECURITY_CORS_ENABLED; disabled → 빈 source (CORS inactive) actually-implemented
origins (D9 allowlist) allowedOriginsAPP_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) allowCredentialsAPP_SECURITY_CORS_ALLOW_CREDENTIALS actually-implemented
max-age 600s maxAgeSecondsAPP_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. runbook auth-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).
  • 다른 계약 의존:

테스트 계약

  • token 값이 log에 나오면 실패.
  • expired token과 invalid signature가 같은 internal code로 뭉개지면 실패.
  • public path snapshot diff 검사: SECURITY_PUBLIC_PATHS env SSOT를 docs/security/public-paths-snapshot.txt와 비교하는 ./gradlew verifyPublicPathSnapshot을 사용하고, 의도한 변경은 -PapprovePublicPathChange 승인 경로로 처리한다. reflection은 폐기됐으며 Java hard-coded permitAll()은 이 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 와 일치 RFC 7519 는 implementer 재량벤더 doc 확인 완료: raw/official-docs/security-spring-jwt-timestamp-validator-clock-skew 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 < (24hcachebuffer) 여야 공식 충족 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 codesRESOLVED 2026-06-08, 옵션 (a)): docs/registries/error-codes.yaml12 fine-grained AUTH/AUTHZ/INTERNAL codes 를 production OperationalError enum 에 추가하고, custom EnvelopeAuthenticationEntryPoint+EnvelopeAccessDeniedHandler+SecurityErrorClassifierJwtValidationException/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_UNKNOWN retryable 정합 (drift 아님, 기록용): registry L124 가 2026-06-01 retryable: 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는 별도 재확인 필요.

묶음

본 feature branch 는 leaf — 자식 자료 없음. Phase C2 실 코드 작성 단계에서 errors / interview prep / lectures 가 누적되면 본 섹션에서 그룹화.

근거 자료

오류 기록 (본 feature 작업 중 발생)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

Blog topics

관련 일일 노트

이 브랜치를 작업한 날짜들. 양방향 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 거부 / verifyPublicPathSnapshot gate / token redaction contract test
    • prod-verified 항목: (없음 — 실 IdP 연동 통합 테스트 미수행)
  • 추출하지 않을 항목 (planned / documented-only / abandoned): JWKS 10min cache + 1/min rate-limit (Minimal 결정, exact number NEEDS_CONTEXT) / 24h rotation overlap (IdP-side) / 실 토큰 서명 통합 테스트(IdP 필요)