491 lines
53 KiB
Markdown
491 lines
53 KiB
Markdown
---
|
||
title: branch / feature-security-operational-baseline
|
||
source_type: branch-note
|
||
status: raw
|
||
branch: feature-security-operational-baseline
|
||
parent_branch:
|
||
related_projects: [ca-skeleton]
|
||
governing_docs: [wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets.md]
|
||
tags: [branch, ca-skeleton, security, jwt, authentication, authorization]
|
||
created: 2026-05-21
|
||
target_merge:
|
||
status_label: in-progress
|
||
last_implementation: 2026-06-08
|
||
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-008
|
||
kind: project-work-item
|
||
project: ca-skeleton-operational-contract
|
||
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-008
|
||
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1]
|
||
refines: []
|
||
overrides: []
|
||
depends_on: []
|
||
contract_packet: 1
|
||
contract_packet_sha256: 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 기준의 인증/인가 실패 운영 분류를 정의합니다.
|
||
|
||
<!-- section-id: branch-parent -->
|
||
## 부모 (필수)
|
||
|
||
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
|
||
|
||
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.
|
||
|
||
<!-- GENERATED: branch-contract:start -->
|
||
<!-- section-id: branch-contract-packet -->
|
||
## 브랜치 계약 패킷
|
||
|
||
- **생성 시 프로젝트 개정**: `1`
|
||
- **패킷 스키마**: `contract_packet: 1`
|
||
- **완료 조건**: security failure·header contract와 negative test가 통과한다
|
||
|
||
<!-- section-id: inherited-project-decisions -->
|
||
### 상속한 프로젝트 결정
|
||
|
||
| 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]] |
|
||
|
||
<!-- section-id: branch-local-decisions -->
|
||
### 브랜치 지역 결정
|
||
|
||
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
|
||
|
||
| Decision ID | Decision | Relation | Supporting Claims | Status |
|
||
|---|---|---|---|---|
|
||
|
||
<!-- section-id: declared-overrides -->
|
||
### 선언한 예외
|
||
|
||
| Override ID | Overrides | Reason | Approval | Status |
|
||
|---|---|---|---|---|
|
||
<!-- GENERATED: branch-contract:end -->
|
||
|
||
<!-- section-id: branch-goal -->
|
||
## 목표
|
||
|
||
Security 실패를 401/403으로만 처리하면 운영자가 missing token, expired token, issuer mismatch, public path misconfiguration을 구분할 수 없습니다. 클라이언트 응답은 과노출하지 않고 내부 로그에는 안전한 분류 code를 남깁니다.
|
||
|
||
- 이슈:
|
||
- PR:
|
||
|
||
<!-- section-id: branch-scope -->
|
||
## 범위
|
||
|
||
### 포함 범위
|
||
|
||
- 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 5~10min 권고 + overlap 공식(token TTL+cache TTL+10min) (WORKOS-JWKS-C1~C4) — `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.
|
||
|
||
- **채택 결정 (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-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 `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배 → 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 감지 시 `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/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#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. 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).
|
||
- **다른 계약 의존**:
|
||
- [[raw/branch-notes/feature-operational-error-observability-foundation]] `D10` — `Category` 10-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` 가 이 패턴을 따름.
|
||
|
||
## 테스트 계약
|
||
|
||
- 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 < (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** 를 production `OperationalError` enum 에 추가하고, custom `EnvelopeAuthenticationEntryPoint`+`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_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는 별도 재확인 필요.
|
||
|
||
## 묶음
|
||
|
||
<!-- GENERATED: sources:start -->
|
||
- [[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]]
|
||
<!-- GENERATED: sources:end -->
|
||
|
||
<!-- GENERATED: interviews:start -->
|
||
- [[raw/interviews/jwt-resource-server-fine-grained-error-classification-2026-06-08]]
|
||
<!-- GENERATED: interviews:end -->
|
||
|
||
<!-- GENERATED: errors:start -->
|
||
- [[raw/errors/internal-auth-misconfiguration-retryable-invariant-conflict-2026-06-08]]
|
||
<!-- GENERATED: errors:end -->
|
||
|
||
<!-- GENERATED: blog-topics:start -->
|
||
- [[raw/blog-topics/spring-security-filter-layer-error-envelope-2026-06-08]]
|
||
<!-- GENERATED: blog-topics:end -->
|
||
|
||
> 본 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 `JwtTimestampValidator` default leeway = 60s (SS-JTVC-C1, `official-vendor-doc`). 2026-06-08 `wiki-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_MISCONFIGURATION` retryable=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 거부 / `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 필요)
|