Files
llm-wiki/raw/branch-notes/feature-secrets-config-source-contract.md

45 KiB

title, source_type, status, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, 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 id kind project work_item inherits refines overrides depends_on contract_packet contract_packet_sha256
branch / feature-secrets-config-source-contract branch-note raw feature-secrets-config-source-contract
ca-skeleton
wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets.md
branch
ca-skeleton
secrets
config
2026-05-22 in-progress BR-CA-SKELETON-OPERATIONAL-CONTRACT-020 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-020
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1
1 0864989d368f669d535a56b56c93b169c38fa14fcd838ac3efad1eea0d6b7b8a

branch: feature-secrets-config-source-contract

Layer: raw/branch-notes/ — secret과 config source 기준을 정의합니다.

부모 (필수)

ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: secret source·classification·leakage 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

목표

env-driven configuration만으로는 secret 관리 기준이 부족합니다. local .env, prod secret source, config dump 금지, rotation 고려를 skeleton 계약에 포함해야 합니다.

  • 이슈:
  • PR:

범위

포함 범위

  • env vs secret manager 사용 범위.
  • local .env 허용 기준.
  • prod secret 노출 금지.
  • config dump 금지.
  • secret masking 기준.
  • secret rotation 고려.
  • startup secret validation.

제외 범위

  • 특정 secret manager 구현.
  • cloud IAM policy 작성.
  • 실제 secret rotation job 구현.

근거 (필수, 최소 1개+)

본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.

Source 정당화하는 결정
raw/official-docs/secrets-aws-secrets-manager-rotation AWS Secrets Manager + auto-rotation (managed Lambda; ca-tmpl dual-bind 60s 패턴과 정합
raw/official-docs/secrets-vault-dynamic-secrets-hashicorp short lease 보안 우위 vs connection pool lifecycle 충돌 + Vault SPoF; ca-tmpl @RefreshScope 금지와 정면 충돌
raw/official-docs/secrets-k8s-secret-external-secrets-operator "mounted env" 경로 실 구현; etcd unencrypted 한계 그대로
raw/company-tech-blogs/secrets-1password-developer-secret-references developer machine까지 reference 보호 vs SaaS 의존
raw/official-docs/config-spring-boot-externalized-configuration @ConfigurationProperties startup 바인딩 모델 (SPRING-EXTCONFIG-C5) — D3 restart-only 의 derived 근거(config 는 startup-bound, reload 는 별도 opt-in machinery 필요) + §2 startup validation(@Validated) 메커니즘

외부 근거 / 대안 조사 (2026-05-22 — Group G-B: Secrets / Config Source)

본 branch의 prod=secret manager OR mounted env + rotation restart-only default + HMAC salt 90d rotation + __LOCAL_DEV_ sentinel 결정에 대한 외부 source.

  • 채택 결정 (managed secret manager + restart-only rotation):
  • 검토한 대안:
  • 비교 핵심: Vault dynamic은 short lease 강점이나 @RefreshScope 금지와 충돌, SPoF risk. AWS Secrets Manager auto-rotation이 ca-tmpl dual-bind 60s 패턴과 가장 정합. ESO는 K8s native이나 etcd 한계, Doppler/1Password는 dev machine까지 보호하나 SaaS 의존성 trade-off.

TODO

TODO drained 2026-05-22 — 결정은 아래 "결정 사항" / "Secret Source Defaults" 참조. env vs secret manager 사용 범위 / local .env 허용 / prod 노출 금지 / config dump 금지 / masking / rotation / startup validation 기준 모두 결정 라인 또는 표로 반영됨. 잔존 TODO 없음.

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 승급 위치

진행 중 메모

  • secret은 log, actuator, error response, configprops 노출과 연결됩니다.

결정 사항 (decisions)

  • 2026-05-22: secret/config source를 env runtime configuration에서 분리해 관리.
  • 2026-05-22: local .env는 local/dev only, prod는 external secret manager 또는 mounted secret file/env injection을 사용.
  • 2026-05-22: secret reload 기본값은 no runtime reload. rotation은 restart validation을 기본으로 하고 runtime reload는 별도 contract 필요.
  • 2026-05-22: secret classification은 public-config, sensitive-config, secret 3단계.
  • 2026-05-22: prod secret source = AWS Secrets Manager 또는 GCP Secret Manager 또는 HashiCorp Vault 중 platform 표준. env 직접 주입은 cloud secret injection (mounted env)만 허용.
  • 2026-05-22: secret rotation 책임 = (a) JWT signing key는 24h overlap window 유지 (security branch와 cross-link), (b) DB credential은 dual-bind 60s, (c) external API key는 application restart 시 reload.
  • 2026-05-22: secret classification = registry-managed (contract-registry-governance의 secrets registry). naming pattern은 보조(suffix _TOKEN, _KEY, _PASSWORD).
  • 2026-05-22: dev/local sentinel value prefix = __LOCAL_DEV_ (예: __LOCAL_DEV_FAKE_DB_PASSWORD). prod profile에서 이 prefix 발견 시 startup fail.
  • 2026-05-22: JWT signing key rotation overlap window(24h) 결정은 security-operational-baseline과 정합. JWKS refresh 운영 정책은 security branch consume. 본 branch는 key 저장/주입/rotation 도구 결정만.

Secret Source Defaults

item default forbidden
local source .env allowed .env in prod
prod source external secret manager or mounted secret plain config file committed
reload restart required silent runtime reload
masking full mask except last 4 chars for non-secret tokens partial token in log
classification public/sensitive/secret unclassified config

결정-근거 매핑

본 branch 의 각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시한다. Decision ID 는 안정적으로 유지한다. company-tech-blog 출처는 company-case-study 로 표기하며 공식 best practice 로 일반화하지 않는다.

선택 조건 열(R2): 분기가 없는 결정(분류 자체가 필수이거나 다른 branch 위임)은 N/A + 한 줄 이유.

Decision ID Decision 선택 조건 (언제 이 결정 / 언제 대안) Supporting Claims Evidence Strength Open Risk
D1 secret/config source 를 env runtime configuration 에서 분리해 관리 값의 classification tier 가 sensitive-config/secret (노출 시 영향 有) 이면 secret source 로 분리, public-config(profile/port/name) 이면 env runtime config 그대로 → tier 가 분기 기준 (D4) raw/official-docs/config-12-factor-app-config.md#TWELVE-FACTOR-CONFIG-C4 (litmus test: open source 시 credential 노출 금지) official-reference 12-factor §III 는 secret 의 별도 저장소를 명시하지 않음 — 분리 필요성만 시사. 안전한 저장소 선택은 별도
D2 local .env = local/dev only, prod = external secret manager OR mounted secret/env injection active profile 이 prod 이면 secret manager/mounted env 강제(.env 금지), local/dev 이면 .env 허용 → active profile 이 분기 기준 raw/official-docs/secrets-aws-secrets-manager-rotation.md#AWS-SM-ROTATE-C1, raw/official-docs/secrets-k8s-secret-external-secrets-operator.md#K8S-ESO-C2, raw/official-docs/secrets-k8s-secret-external-secrets-operator.md#K8S-ESO-C3 official-vendor-doc (AWS + K8s) plain K8s Secret 은 etcd unencrypted (C2) + API full read (C3) 한계. ESO + Encryption at Rest 별도 활성화 필요
D3 secret reload 기본값 = no runtime reload (rotation = restart validation) 기본은 모든 secret = no-runtime-reload; 명시적 rotation handler(예: JwtSigningKeyRotator) 가 별도 contract 로 등록된 secret 에 한해 in-process rotation 허용 → 명시적 handler 유무가 분기 기준 DERIVED (positive vendor claim 아님): raw/official-docs/config-spring-boot-externalized-configuration.md#SPRING-EXTCONFIG-C5 (config 는 @ConfigurationPropertiesstartup 바인딩 되는 모델) + D10(reload opt-in 금지) + raw/official-docs/secrets-vault-dynamic-secrets-hashicorp.md#VAULT-DYN-C2 (reload 는 lease/@RefreshScope 같은 명시적 machinery 를 요구). 세 근거의 합 = reload 경로가 opt-in 인데 본 계약이 opt-in 을 금지 → restart-only. AWS-SM-ROTATE-C2 는 secret-store 측 rotation 만 증명(app 전파 미증명) derived (opt-in reload machinery 부재) + official-vendor-doc (secret-store 측만) restart-only 의 핵심 전제 = "app 이 AWSCURRENT 변경을 자동 전파하지 않는다"는 추론(reload machinery 미도입). 실측 확정은 §Claims To Verify 의 SecretReloadContractTest(planned) — Vault 대안의 lease 자동 reload 도 app 측 로직 필요(보장 안 됨)
D4 secret classification 3단계 = public-config, sensitive-config, secret N/A — 분류 자체는 모든 registry 등록 config 에 필수(분기 아님). tier 판정 기준 = 값 노출 시 영향(none→public, 제한적→sensitive, 직접 credential→secret). UNSUPPORTED_DECISION UNSUPPORTED_DECISION — 분류 체계는 branch 자체 정합성 규칙. 외부 official 분류 표준 raw 미확보 (NIST/ENISA data-classification 은 이 3-tier 와 1:1 매핑되지 않음) none ENISA / NIST classification 표준 raw 미확보. registry-managed metadata 의 운영 합리성은 별도. trade-off: 외부 표준 대신 노출-영향 기반 3-tier 를 선택(운영 단순성 우선)
D5 prod secret source 를 스왑 가능 SecretSource 포트 + EnvironmentSecretSource 기본 + factory 로 제공 (AWS SM/GCP SM/Vault 는 예약 strategy) — 2026-06-09 추상화 승급 기본 ENVIRONMENT(Spring Env). 배포 platform 이 AWS/GCP/self-managed 면 해당 strategy 추가(새 SecretSource impl + factory case)로 스왑 — ca-skeleton.secret-source.strategy raw/official-docs/secrets-aws-secrets-manager-rotation.md#AWS-SM-ROTATE-C1 ~ C4 (rotation 3 모델 공식 정의), raw/official-docs/secrets-vault-dynamic-secrets-hashicorp.md#VAULT-DYN-C4 (Vault static role 지원) official-vendor-doc (AWS + Vault) GCP Secret Manager 의 rotation 모델 raw 미확보. "platform 표준" 의 정량 기준은 운영 결정
D6 DB credential rotation = dual-bind (window 값 60s) DB credential 처럼 무중단 rotation 이 필요한 secret 은 dual-bind window(old+new 동시 유효), 무중단 불요(API key 등) 면 restart-only → 무중단 요구 여부가 D6/D7 분기 기준 dual-bind 메커니즘: raw/official-docs/secrets-aws-secrets-manager-rotation.md#AWS-SM-ROTATE-C4 (Lambda multi-user rotation 존재 = L1). window 값 60s 는 UNSUPPORTED_IMPL_DECISION — 근거 raw 없음(§구현 가이드 §5 참조) official-vendor-doc (dual-bind 메커니즘만) · 60s 값 = none dual-bind 채택은 근거 있음(multi-user rotation 모델). 60s 는 ca-tmpl 운영 default 로 근거 없음 — AWS multi-user strategy default window 와 일치하는지는 §Claims To Verify(needs-confirmation). trade-off: window 가 짧을수록 노출 창 ↓ 이나 양측 갱신 동기화 압박 ↑
D7 external API key rotation = application restart 시 reload 외부 API key 는 무중단 요구 낮고 의존 adapter 가 restart 로 재초기화되므로 restart-reload; 무중단 필수면 D6 의 dual-bind 채택 → D6 과 동일 분기(무중단 요구) raw/official-docs/secrets-aws-secrets-manager-rotation.md#AWS-SM-ROTATE-C1 (rotation = secret + service 양측 업데이트) official-vendor-doc "restart 시 reload" 는 ca-tmpl restart-only 정책의 운영 선택
D8 JWT signing key rotation overlap window = 24h N/A (DELEGATED) — overlap window 값(24h)은 본 branch 결정 아님. 본 branch 는 APP_SECURITY_JWT_SIGNING_KEY 저장/주입/분류(secret, source=secret-manager)만 소유 DELEGATED → raw/branch-notes/feature-security-operational-baseline (registry secrets-classification.yaml APP_SECURITY_JWT_SIGNING_KEY rotation_policy: overlap-24h, owner_branch cross-link) delegated overlap window 의 official 근거는 security branch 가 보유해야 함(JWKS/OIDC spec). 본 branch 는 정합성만 — 그 값이 바뀌면 registry row 동기화 필요
D9 dev/local sentinel value prefix = __LOCAL_DEV_ (prod profile 발견 시 startup fail) N/A — prod profile 에서 값이 __LOCAL_DEV_ 로 시작하면 무조건 startup fail(분기 아닌 guard). dev/local 에서는 fake credential 로 허용. UNSUPPORTED_DECISION UNSUPPORTED_DECISION — branch 자체 정합성 규칙 (local fake credential 의 prod 누출 차단). prefix 문자열 convention 의 외부 official 표준 없음 none sentinel prefix convention 의 외부 official 근거 없음. trade-off: 별도 vault 격리 대신 값 prefix + startup guard 로 prod 오탑재 차단(구현 단순성 우선)
D10 secret reload 정적 강제 = @RefreshScope 금지 contract test (SecretReloadContractTest) N/A — D3(no-runtime-reload)의 정적 강제 이므로 분기 없음. D3 의 명시적 rotation handler carve-out 만 예외 D3 derive — D3 의 AWS-SM-ROTATE-C2 + VAULT-DYN-C2(dynamic 거부) 가 근거. Spring @RefreshScope 메커니즘 자체는 사실이나 reference doc raw 미확보(§Claims To Verify) derived (D3) Spring Cloud @RefreshScope reference doc raw 미확보 — 메커니즘 사실 확인용 follow-up

구현 가이드

결정(Decisions) 이 "무엇" 이라면 본 §는 "어디에 어떻게" — 구현 착수 가능 수준의 명세. 상태 = locally-verified(2026-06-08 구현 완료). 아래 C1C3 모두 코드 작성 + 테스트 통과. actually-implemented 표기 항목은 registry yaml + C1C3 산출물.

구현 결과 (2026-06-08, locally-verified)

§0 의 C1~C3 3개 산출물을 ca-tmpl 의 기존 패턴에 정합시켜 구현 완료. 변경 파일:

# 파일 종류 근거 패턴
C1 src/app-bootstrap/.../bootstrap/runtime/SecretSourceValidator.java 신규 production (SmartInitializingSingleton, 1-arg ConfigurableEnvironment) StartupSafetyValidator
C1 src/app-bootstrap/.../bootstrap/runtime/SecretSourceConfig.java 신규 production wiring (@Configuration @Bean) RuntimeSafetyConfig
C1 src/app-bootstrap/src/test/.../bootstrap/runtime/SecretSourceValidatorTest.java 신규 test (7 케이스, ApplicationContextRunner) StartupSafetyValidatorTest
C2 src/app-bootstrap/src/test/.../bootstrap/contract/SecretsClassificationRegistryTest.java 신규 test (registry↔상수 drift, snakeyaml + assumeTrue SKIP) RepositoryAccessCapabilityRegistryTest / ErrorCodeRegistryMappingTest
C3 CleanArchitectureTest.java (+1 @ArchTest no_refresh_scope_anywhere, FQN string beAnnotatedWith) 기존 파일 수정 기존 noClasses() ArchRule
C3 .../architecture/violations/secrets/RefreshScopeUsingFixture.java 신규 test fixture (@RefreshScope) SpringWebSocketHandlerFixture
C3 ArchitectureViolationFixtureTest.java (+1 fixture 검증 테스트, importPackages) 기존 파일 수정 기존 violations-as-data 패턴
C3 src/app-bootstrap/build.gradle (+testCompileOnly 'org.springframework.cloud:spring-cloud-context:4.1.4') 기존 파일 수정 기존 streaming testCompileOnly fixture deps
§4 보강 src/app-bootstrap/src/test/.../bootstrap/contract/SecretReloadContractTest.java (신규, 2 케이스) §4 "선택" 런타임 보강 — 구현함 ApplicationContextRunner + startup-binding immutability

§4 SecretReloadContractTest(원래 "선택/우선순위 낮음/planned")도 구현: (1) startup-bound @ConfigurationProperties 값이 context refresh 후 property source 주입에도 불변(no auto-reload, SPRING-EXTCONFIG-C5), (2) org.springframework.cloud.context.scope.refresh.RefreshScope 가 runtime classpath 에 부재(testCompileOnly)함을 단언 → in-process reload 경로 자체가 없음을 infra 레벨로 증명. 이로써 spec 본문에 이름이 명시된 산출물 중 미구현 0건.

결정 PIN 그대로 적용: SecretSourceValidator 1-arg ctor(ConfigurableEnvironment만), REQUIRED_PROD_SECRETS = registry classification: secret + prod_default: null 6 key 와 C2 가 1:1 단언(drift 시 build fail), @RefreshScope 전면 금지(carve-out 없음, FQN 문자열 참조). REQUIRED_PROD_SECRETSpublic(C2 가 cross-package …contract 에서 읽어야 하므로 — StartupSafetyValidator.REQUIRED_MULTI_INSTANCE_BEANS 의 package-private 와 다른 의도적 차이).

검증: ./gradlew :app-bootstrap:test(21 class 전체 PASS, 0 skip — docs/ 존재 시 C2 drift 단언 실측 통과) + verifyCleanArchitectureDependencies PASS. spring-cloud-context 4.1.4 Maven Central 해결 성공. 커밋은 사용자가 직접 수행(미커밋 상태).

원본 설계 명세 (구현 전 PIN, 참조용 보존)

아래 클래스·테스트·패키지·메커니즘은 ca-tmpl 의 기존 패턴에 정합시켜 확정(추측 아님) — 근거 패턴을 각 항목 Trace 에 실제 파일로 명시한다.

0. 본 branch 코드 산출물 (3개 — in-scope)

§범위 In scope 중 본 branch 가 코드로 만드는 것. rotation job 구현·secret manager SDK 통합·masking 강제 지점은 §범위 Out of scope 또는 위임(§5·§6).

# 산출물 종류 위치 (module / package) 근거 패턴 (ca-tmpl 실재 파일)
C1 SecretSourceValidator + SecretSourceConfig 시작 fail-fast guard app-bootstrap / dev.caskeleton.bootstrap.runtime StartupSafetyValidator + RuntimeSafetyConfig (동일 package)
C2 SecretsClassificationRegistryTest registry↔상수 drift 가드 app-bootstrap test / …bootstrap.contract RepositoryAccessCapabilityRegistryTest · ErrorCodeRegistryMappingTest
C3 no_refresh_scope_anywhere ArchRule + violation fixture 정적 강제 app-bootstrap test / …bootstrap.architecture (+ architecture/violations/secrets/) CleanArchitectureTest + architecture/violations/** fixture

(registry secrets-classification.yaml 자체는 이미 존재 = C2 가 가드할 대상. C1~C3 외 신규 production 클래스 없음.)

1. Secret classification registry (3-tier) — 계약 SSOT (registry 실재)

Trace: D4(3-tier) + §Secret Source Defaults(masking). 근거 산출물 = secrets-classification.yaml(실재). 아래 표 = registry 의 view. tier 경계 기준·masking 선택은 D4 의 결정(노출-영향 기반)이며 외부 표준 미매핑은 D4 Open Risk 로 남김(impl 임의 아님). registry schema/키 명명feature-contract-registry-governance 소유(OUT_OF_BRANCH).

tier source (기본) masking_rule 예시 key (registry 실재 row)
secret secret-manager full (API key 는 full_except_last_4) APP_DATASOURCE_PASSWORD, APP_SECURITY_JWT_SIGNING_KEY, APP_SECURITY_OAUTH_CLIENT_SECRET, APP_EXTERNAL_API_KEY, APP_CACHE_REDIS_PASSWORD, APP_PRIVACY_PSEUDONYMIZATION_SALT
sensitive-config mounted-env (또는 secret-manager) full_except_last_4 APP_DATASOURCE_USERNAME, APP_DATASOURCE_URL, APP_SECURITY_GOOGLE_OAUTH_CLIENT_ID, APP_NOTIFICATION_SLACK_WEBHOOK_URL
public-config application-yml none APP_PROFILE, APP_NAME, SERVER_PORT, SPRING_PROFILES_ACTIVE (reference only — full row 는 env-keys.yaml)

APP_PRIVACY_PSEUDONYMIZATION_SALT 는 row 만 본 registry 에 있으나 owner_branch: feature-data-retention-privacy-contract — 분류 tier 는 본 계약, rotation(90d)은 위임(§5).

  • C2 SecretsClassificationRegistryTest (…bootstrap.contract, test): registry↔as-built drift 가드. snakeyaml Yamldocs/registries/secrets-classification.yaml 로드 → classification: secret + prod_default: null row 집합이 SecretSourceValidator.REQUIRED_PROD_SECRETS 상수와 1:1 일치, 그 외 row 의 tier 값이 enum(public-config/sensitive-config/secret)에 속함을 단언. docs/ 는 repo gitignore 대상 → 부재 시 Assumptions.assumeTrue(...)SKIP(통과 아님) (= RepositoryAccessCapabilityRegistryTest / ErrorCodeRegistryMappingTest 패턴 1:1).

2. SecretSourceValidator — sentinel + required-secret 시작 검증 (C1)

Trace: D9(sentinel) + §테스트 계약("required secret 누락 시 startup 성공하면 실패"). 근거 패턴 = src/app-bootstrap/.../bootstrap/runtime/StartupSafetyValidator.java(SmartInitializingSingleton) + wiring RuntimeSafetyConfig.java.

  • 메커니즘 PIN = SmartInitializingSingleton (이전 EnvironmentPostProcessor 후보를 폐기). 근거: ca-tmpl 의 시작 검증이 이미 StartupSafetyValidatorSmartInitializingSingleton 에 통일돼 있고(그 Javadoc 이 EPP/ApplicationReadyEvent 대비 timing 근거를 명시), 본 검증도 같은 prod-profile + Environment 값 검사 부류 → 동일 메커니즘이 일관적.
  • 잔여 trade-off(명시): SmartInitializingSingleton 은 singleton 인스턴스화 실행 → eager DataSource__LOCAL_DEV_ 자격으로 먼저 connect 시도 가능. 더 이른 차단이 필요하면 EnvironmentPostProcessor 로 승격(별도 메커니즘 추가 비용). prod 에서 __LOCAL_DEV_ 도달 자체가 예외적 오탑재이고 context refresh 완료(=트래픽 수용) 전 abort 되므로 본 PIN 으로 충분 판단.
  • dev.caskeleton.bootstrap.runtime.SecretSourceValidator implements SmartInitializingSingleton — plain class(단위테스트 가능, StartupSafetyValidator 와 동일 구조). ctor (ConfigurableEnvironment environment)1-arg(기준 StartupSafetyValidator 는 3-arg Environment + RuntimeSafetySettings + ListableBeanFactory 이나, 본 검사는 bean-presence 조회 불요·RuntimeSafetySettings 미사용·Environment property 값만 필요 → 의도적 단순화). afterSingletonsInstantiated() 가 아래 두 검사 호출:
    • validateNoLocalDevSentinelInProd(): prod active 시 environment.getPropertySources() 의 각 EnumerablePropertySource 값 스캔 → __LOCAL_DEV_ 로 시작하는 값 발견 시 위반 key 나열한 IllegalStateException throw(context refresh 중단).
    • validateRequiredSecretsPresent(): prod active 시 in-code 상수 REQUIRED_PROD_SECRETS(= registry classification: secret + prod_default: null key 목록; StartupSafetyValidator.REQUIRED_MULTI_INSTANCE_BEANS 와 동일한 상수 패턴) 의 각 key environment.getProperty(key) 가 blank → 누락 key 나열 throw. dev/local 은 검사 skip(__LOCAL_DEV_* fallback 허용).
  • wiring: dev.caskeleton.bootstrap.runtime.SecretSourceConfig(@Configuration) 가 @Bean SecretSourceValidator(ConfigurableEnvironment) 등록(= RuntimeSafetyConfig 패턴; 소유권 분리 위해 별도 config). composition-root 외 production wiring 없음.
  • test: SecretSourceValidatorTest(…bootstrap.runtime, test) — ApplicationContextRunner + .withInitializer(ctx→getEnvironment().setActiveProfiles("prod")) + .withPropertyValues(...) + assertThat(context).hasFailed() & getStartupFailure().hasStackTraceContaining("<key>") (= StartupSafetyValidatorTest 패턴 1:1).

3. Secret source resolution — 스왑 가능 SecretSource 포트 + Environment 기본 (2026-06-09 추상화 승급)

Trace: D2 + D5 + AWS-SM-ROTATE-C1, K8S-ESO-C2/C3.

2026-06-09 갱신 (abstraction-gap 해소): 초안은 "본 branch 는 커스텀 resolver 를 만들지 않는다 / D5 는 enum 만 고정"이었으나, rate-limit 선례(RateLimiter 포트 + 기본 + factory 스왑)에 비춰 secret source 야말로 스왑 1순위 후보(env/Vault/AWS SM/GCP SM 은 진짜 대안)인데 포트가 없어 registry source: 텍스트가 죽은 분류값이었음. → SecretSource 포트 + EnvironmentSecretSource 기본 + SecretSourceStrategy enum + SecretSourceFactory + SecretSourceProperties 선박. SecretSourceValidator 의 required-secret 검사가 이제 포트(secretSource.resolve(key))를 경유 → backend 스왑을 따라감.

요소 클래스 비고
포트 SecretSource (Optional<String> resolve(key)) blank=absent 강제
기본 구현 EnvironmentSecretSource Spring Environment 위임 (= 기존 동작)
strategy enum SecretSourceStrategy (ENVIRONMENT 기본; VAULT/AWS_SECRETS_MANAGER/GCP_SECRET_MANAGER 주석)
factory(확장점) SecretSourceFactory (switch)
설정 스왑 SecretSourceProperties (ca-skeleton.secret-source.strategy, 기본 ENVIRONMENT)
  • PIN(유지): ENVIRONMENT 기본은 Spring Boot 표준 PropertySource 우선순위(OS env/mounted > application.yml)에 위임 — prod 주입은 Spring 이 이미 우선 적용. 아래 표는 허용/금지 계약, 강제 지점은 §2 + §4 + §6.
  • OUT_OF_BRANCH(유지): 구체 secret manager SDK 연결(AWS/GCP SDK, Vault agent)은 adapter/future — VAULT/AWS_SECRETS_MANAGER strategy 는 enum 주석 + factory 확장점으로 예약(미선박). registry per-row source: 는 분류 메타로 잔존(글로벌 backend 선택은 strategy 가 담당).
active profile 허용 source 금지
local / dev .env (+ __LOCAL_DEV_ sentinel), application-yml(public) committed plain config 에 secret
prod secret-manager OR mounted-env(cloud secret injection) .env, committed plain config

4. @RefreshScope 전면 금지 — no-runtime-reload 정적 강제 (C3)

Trace: D3 + D10 + VAULT-DYN-C2(dynamic 거부). 근거 패턴 = src/app-bootstrap/.../bootstrap/architecture/CleanArchitectureTest.java(@AnalyzeClasses(packages="dev.caskeleton", DoNotIncludeTests), noClasses()… ArchRule) + architecture/violations/** fixture.

  • 금지 범위 PIN = 전면 금지(carve-out 없음). 근거: src 전체 @RefreshScope 0건(2026-06-06 grep) → 전면 금지가 안전하고 단순. 이전 "registry 파생 carve-out + handler 식별 표지" 는 불요로 폐기 — 허용된 rotation handler(JWT overlap 등, 다른 branch 소유)는 @RefreshScope 가 아니라 명시적 mutable holder + scheduled swap 으로 in-process rotation 하므로 @RefreshScope 를 쓸 일이 없다. 따라서 식별 marker 도 불필요.
  • no_refresh_scope_anywhere ArchRule: @AnalyzeClasses(packages="dev.caskeleton") 스위트에 @ArchTest static final ArchRule 추가 — noClasses().should().beAnnotatedWith("org.springframework.cloud.context.config.annotation.RefreshScope") (spring-cloud classpath 부재 가능 → FQN 문자열로 참조). 위반 시 build fail.
  • violation fixture: dev.caskeleton.bootstrap.architecture.violations.secrets.RefreshScopeUsingFixture(test fixture, @RefreshScope 부착) + ArchitectureViolationFixtureTest 가 룰이 실제로 잡는지 양성 검증(기존 violations/** 패턴 1:1). 로딩은 importPackages("…violations.secrets") 사용(importClasses 는 Spring Cloud 가 testCompileOnly 일 때 link-time class load fail 위험 — ArchitectureViolationFixtureTestSPRING_WEBSOCKET_FIXTURE_ONLY 격리 패턴 참고).
  • 런타임 검증 보강(선택, SecretReloadContractTest): secret 값 변경 후 application 이 자동 reload 안 함을 ApplicationContextRunner 로 verify. 정적 ArchRule 이 1차 방어이므로 우선순위 낮음(needs-confirmation 의 AWSCURRENT 전파 항목과 짝).

5. Rotation policy 매핑 (per-secret) — registry 값만, job 구현은 out-of-scope (delegated)

Trace: D6(DB dual-bind, AWS-SM-ROTATE-C4) + D7(API restart-reload) + D8(JWT 24h, DELEGATED) + HMAC salt 90d(DELEGATED). 값은 registry rotation_policy 컬럼에 실재.

  • §범위 Out of scope: "실제 secret rotation job 구현". 본 branch 는 registry rotation_policy 만 소유하고 rotation 메커니즘 코드(handler) 는 만들지 않는다 → §0 코드 산출물(C1~C3)에 rotation handler 없음.

  • OUT_OF_BRANCH_SCOPE: overlap-24h(JWT signing key) → feature-security-operational-baseline; salt-rotation-90d(pseudonymization salt) → feature-data-retention-privacy-contract. 본 branch 는 registry rotation_policy enum 값 등록만, 실제 rotation 메커니즘/주기 근거는 owner branch.

  • UNSUPPORTED_IMPL_DECISION: dual-bind window 값 60s(D6) — dual-bind 메커니즘AWS-SM-ROTATE-C4 로 근거 있으나 60s 라는 값은 근거 raw 없음(AWS multi-user strategy default 와 일치 여부 needs-confirmation). trade-off: window ↓ = 노출 창 ↓ / 양측(old·new) 갱신 동기화 압박 ↑. 30s·90s 도 가능했던 운영 임의값.

secret rotation_policy (registry) owner
APP_DATASOURCE_PASSWORD / APP_DATASOURCE_USERNAME dual-bind-60s 본 branch (D6)
APP_EXTERNAL_API_KEY / APP_SECURITY_OAUTH_CLIENT_SECRET / APP_CACHE_REDIS_PASSWORD restart-only 본 branch (D7)
APP_SECURITY_JWT_SIGNING_KEY overlap-24h raw/branch-notes/feature-security-operational-baseline (D8 위임)
APP_PRIVACY_PSEUDONYMIZATION_SALT salt-rotation-90d feature-data-retention-privacy-contract (위임)

6. Masking & exposure boundary — 분류는 본 branch, 강제는 위임 (delegated)

Trace: §Secret Source Defaults(masking) + §테스트 계약(config dump/log 노출 금지). 본 branch 는 masking_rule 분류값(full / full_except_last_4 / none)만 정의.

  • OUT_OF_BRANCH_SCOPE: actuator /configprops·/env masking 강제 지점 → feature-management-actuator-security-contract; log 출력 masking converter → feature-log-management-contract. 본 branch 는 무엇을 어떻게 마스킹할지의 분류 만 제공하고, 어디서 강제하는지 는 두 sibling 이 consume.

엣지·실패·의존

R4 캡처. 정상 경로(prod 에서 secret manager 주입) 외의 실패/엣지/cross-contract 의존.

  • 실패·엣지 경로:
    • required secret 누락 (prod): classification: secret + prod_default: null key 가 prod 에서 미주입 → startup fail(빈 secret 으로 부팅 금지). dev/local 은 __LOCAL_DEV_* fallback.
    • __LOCAL_DEV_ 누출 (prod): prod profile 에서 __LOCAL_DEV_ prefix 값 발견 → startup fail(§2 guard). dev fake credential 의 prod 오탑재 차단.
    • secret manager 도달 불가 (startup): network/IAM 실패로 secret 조회 불가 → startup fail(silent empty 금지). no-runtime-reload(D3) 이므로 부팅 후 secret manager 장애는 in-memory 기존 값 유지(데이터면 영향 없음).
    • rotation window 경계: dual-bind 60s(D6) window 내 old+new 동시 유효; window 밖 old credential 사용 시 auth fail — rotation job 이 window 안에 양측 갱신 완료해야 함.
    • ESO sync 지연 중 Pod restart (mounted-env/K8s 경로): 외부 secret 이 rotation 됐으나 External Secrets Operator 가 아직 K8s Secret 을 갱신하지 않은 상태(K8S-ESO-C5 default sync interval 1h)에서 Pod restart → 이전 값으로 기동. dual-bind window 안이면 동작, 밖이면 auth fail. 대응(채택 시): ESO sync interval 을 rotation window 보다 짧게 설정 또는 rotation 후 수동 reconcile 트리거 — §Claims To Verify 의 ESO sync 항목으로 확정.
    • @RefreshScope 실수 등록: secret bean 에 @RefreshScope 부착 시 contract test build fail(§4) — runtime 도달 전 차단.
  • 다른 계약 의존:

테스트 계약

  • prod profile에서 secret이 config dump/log에 노출되면 실패.
  • required secret 누락 시 startup이 성공하면 실패.
  • local-only .env 설정이 prod에서 허용되면 실패.
  • masking 없는 secret value 출력은 실패.
  • secret reload 검증: 결정 사항에 따라 secret reload는 no-runtime-reload (재시작 강제). 측정 방법: contract test SecretReloadContractTest에서 secret manager의 secret value 변경 후 application이 자동 reload하지 않음 verify. @RefreshScope bean 등록 시 fail. 단 JwtSigningKeyRotator 같은 명시적 rotation handler는 24h overlap window 결정 사항에 따라 허용.

검증해야 할 주장

Claim Why uncertain How to verify Status
ca-tmpl dual-bind 60s 가 AWS Lambda multi-user rotation default window 와 일치 AWS Secrets Manager rotation 페이지의 multi-user strategy default window 가 별도 페이지에 있어 본 raw 에서 미확인 AWS Secrets Manager User Guide multi-user strategy 페이지 fetch + verbatim 확인 needs-confirmation
AWSCURRENT 변경 시 application 까지 자동 전파 안 되고 restart 필요 restart-only 정책 하에서 secret manager → app 전파 경로 미검증 SecretReloadContractTest 구현 후 secret value 변경 → application 자동 reload 안 함 verify planned
__LOCAL_DEV_ prefix 가 prod 누출 차단에 충분 startup guard 미구현 Spring EnvironmentPostProcessor 또는 @PostConstruct validator 구현 + prod profile + __LOCAL_DEV_* 발견 시 startup fail 통합 테스트 planned
JWT signing key 24h overlap window 가 JWKS 표준 권장값 별도 OIDC/JWKS spec 미확인 OIDC discovery + RFC 7517 (JWK) + RFC 7519 (JWT) 권장 rotation cadence 별도 raw 등록 needs-confirmation
Vault dynamic credential 이 HikariCP lease 만료를 감지하고 refresh 하는 메커니즘 dynamic credential 거부 결정의 기술적 근거 보강 필요 Vault Agent / sidecar 패턴 raw 추가 또는 ca-tmpl 이 dynamic 채택 시 별도 검증 needs-confirmation
GCP Secret Manager 의 rotation 모델이 AWS Secrets Manager 와 동등 GCP Secret Manager raw 미확보 GCP Secret Manager official doc raw 등록 + rotation 모델 비교 needs-confirmation
ESO sync interval (default 1h) 이 ca-tmpl rotation SLA 와 호환 sync interval 의 운영 영향 미확인 K8S-ESO-C5 의 reconcile 메커니즘 측정 + ca-tmpl 채택 SLA 와 비교 needs-confirmation
prod profile 에서 secret 이 config dump / log 에 노출되면 startup fail actuator config endpoint 구성 미확인 actuator /configprops mask 정책 + log masking converter (log-management branch) 통합 테스트 planned

관심사 커버리지

governing doc = wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secretsSecrets / Config 축(§프로젝트 컨텍스트 3번). /coverage 가 재생성하는 초안 — 손유지 금지. 기준: rules/coverage-gate.md.

관심사 (governing doc Secrets 축) 상태 owner 심각도 근거
prod source = secret manager OR mounted env covered-here D2, D5
local 만 .env 허용 covered-here D2 / §3
no-runtime-reload default + @RefreshScope 금지 covered-here D3, D10 / §4
__LOCAL_DEV_ sentinel (prod 오탑재 차단) covered-here D9 / §2
secret classification 3-tier covered-here D4 / §1
masking rule 분류 (full / last-4 / none) covered-here §Secret Source Defaults / §1
DB credential dual-bind 60s covered-here D6 / §5
external API key restart-reload covered-here D7 / §5
startup secret validation (required 누락 시 fail) covered-here §테스트 계약 / §2
JWT signing key 24h overlap rotation delegated raw/branch-notes/feature-security-operational-baseline OK registry owner_branch / D8 / §5
HMAC pseudonymization salt 90d rotation delegated raw/branch-notes/feature-data-retention-privacy-contract OK registry owner_branch / §5
actuator /configprops·/env masking 강제 delegated raw/branch-notes/feature-management-actuator-security-contract OK §엣지·실패·의존 / §6
log 출력 secret masking 강제 delegated raw/branch-notes/feature-log-management-contract OK §엣지·실패·의존 / §6
secrets-classification.yaml schema governance delegated raw/branch-notes/feature-contract-registry-governance OK registry Schema owner 주석

마주친 문제

  • spring-cloud-context 버전 명시 필요 (2026-06-08): @RefreshScope fixture 가 org.springframework.cloud.context.config.annotation.RefreshScope 를 testCompile 시 필요로 하나, Spring Boot BOM 은 spring-cloud 좌표를 관리하지 않음 → testCompileOnly 에 명시 버전(4.1.4) PIN 필요. testCompileOnly 라 런타임 호환성 무관(annotation 만 bytecode 로 읽힘). fixture 로딩은 importClasses 대신 importPackages 로 격리해 testCompileOnly 타입의 link-time 해결 회피(streaming WebSocket fixture 와 동일 근거).
  • REQUIRED_PROD_SECRETS 가시성 (2026-06-08): C2 가 …bootstrap.contract 패키지에서 상수를 읽어야 해 public static final 로 노출. StartupSafetyValidator.REQUIRED_MULTI_INSTANCE_BEANS(package-private, 같은 패키지 테스트)와의 의도적 차이 — drift guard 가 다른 패키지에 있기 때문.

묶음

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

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

  • 표준 errors/ 승급 대상 없음 — §마주친 문제 의 두 항목(spring-cloud-context 버전 PIN, REQUIRED_PROD_SECRETS 가시성)은 build 설정/설계 선택이지 디버깅 세션·실패 테스트가 아님. 별도 raw/errors/ 노트 불필요.

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

  • 후보 질문 seed (별도 raw/interviews/ 노트로 승급하기엔 단편적 — 누적 시 그룹화): (1) "startup fail-fast guard 를 EnvironmentPostProcessor 가 아닌 SmartInitializingSingleton 으로 둔 이유와 trade-off?", (2) "secret no-runtime-reload 를 정적으로 강제하는 방법 — @RefreshScope 금지를 ArchUnit 으로 어떻게 잡고 vacuous-pass 를 어떻게 방어하나?", (3) "registry(yaml)↔코드 상수 drift 를 어떻게 build 에서 가드하고, gitignore 된 SSOT 부재 시 SKIP vs FAIL 을 어떻게 구분하나?".

관련 일일 노트

  • 2026-06-08: §0 C1~C3 구현 완료(locally-verified). :app-bootstrap:test + verifyCleanArchitectureDependencies PASS. 미커밋(사용자 커밋 예정).

완료 후 정리

  • PR 링크:
  • 리뷰 메모:
  • 머지 결과 / 배포 환경:
  • wiki 추출 대상 (verified만, wiki/projects/로만 추출):
    • actually-implemented 항목: registry secrets-classification.yaml
    • locally-verified 항목: C1 SecretSourceValidator/SecretSourceConfig/SecretSourceValidatorTest, C2 SecretsClassificationRegistryTest, C3 no_refresh_scope_anywhere ArchRule + RefreshScopeUsingFixture + fixture 검증 테스트, §4 SecretReloadContractTest(선택 보강도 구현), D5 SecretSource 포트 + EnvironmentSecretSource 기본 + SecretSourceStrategy/SecretSourceFactory/SecretSourceProperties + SecretSourceTest(2026-06-09 추상화 승급; :app-bootstrap:test 140/140 green)
    • prod-verified 항목: 없음 (prod 배포 전)
  • 추출하지 않을 항목 (planned / documented-only / abandoned): §3 source resolution(코드 신규 없음 — Spring-native precedence 위임), §5 rotation job(out-of-scope), §6 masking 강제 지점(delegated → actuator/log branch), §Claims To Verify 의 외부 needs-confirmation 항목(AWS multi-user window 일치 / GCP rotation 동등 / ESO sync 등 — 외부 vendor doc 실측 필요, 코드 산출물 아님)