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 |
|
|
|
2026-05-22 | in-progress | BR-CA-SKELETON-OPERATIONAL-CONTRACT-020 | project-work-item | ca-skeleton-operational-contract | WI-CA-SKELETON-OPERATIONAL-CONTRACT-020 |
|
1 | 0864989d368f669d535a56b56c93b169c38fa14fcd838ac3efad1eea0d6b7b8a |
branch: feature-secrets-config-source-contract
Layer:
raw/branch-notes/— secret과 config source 기준을 정의합니다.
부모 (필수)
- Parent project (canonical SSOT): raw/project-notes/ca-skeleton-operational-contract
ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.
브랜치 계약 패킷
- 생성 시 프로젝트 개정:
1 - 패킷 스키마:
contract_packet: 1 - 완료 조건: 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):
- raw/official-docs/secrets-aws-secrets-manager-rotation — AWS Secrets Manager + auto-rotation (managed Lambda; ca-tmpl dual-bind 60s 패턴과 정합)
- 검토한 대안:
- 대안 1: HashiCorp Vault + dynamic secrets — raw/official-docs/secrets-vault-dynamic-secrets-hashicorp (short lease 보안 우위 vs connection pool lifecycle 충돌 + Vault SPoF; ca-tmpl
@RefreshScope금지와 정면 충돌) - 대안 2: K8s Secret + external-secrets-operator — raw/official-docs/secrets-k8s-secret-external-secrets-operator ("mounted env" 경로 실 구현; etcd unencrypted 한계 그대로)
- 대안 3: Doppler / 1Password SDK — raw/company-tech-blogs/secrets-1password-developer-secret-references (developer machine까지 reference 보호 vs SaaS 의존)
- 대안 4: Plain env (rejected) — prod에서 dump/log 노출 위험으로 ca-tmpl 명시적 거부
- 대안 1: HashiCorp Vault + dynamic secrets — raw/official-docs/secrets-vault-dynamic-secrets-hashicorp (short lease 보안 우위 vs connection pool lifecycle 충돌 + Vault SPoF; ca-tmpl
- 비교 핵심: 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,secret3단계. - 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 는 @ConfigurationProperties 로 startup 바인딩 되는 모델) + 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 모두 코드 작성 + 테스트 통과.C3 산출물.actually-implemented표기 항목은 registry yaml + C1구현 결과 (2026-06-08,
locally-verified)§0 의 C1~C3 3개 산출물을 ca-tmpl 의 기존 패턴에 정합시켜 구현 완료. 변경 파일:
# 파일 종류 근거 패턴 C1 src/app-bootstrap/.../bootstrap/runtime/SecretSourceValidator.java신규 production ( SmartInitializingSingleton, 1-argConfigurableEnvironment)StartupSafetyValidatorC1 src/app-bootstrap/.../bootstrap/runtime/SecretSourceConfig.java신규 production wiring ( @Configuration@Bean)RuntimeSafetyConfigC1 src/app-bootstrap/src/test/.../bootstrap/runtime/SecretSourceValidatorTest.java신규 test (7 케이스, ApplicationContextRunner)StartupSafetyValidatorTestC2 src/app-bootstrap/src/test/.../bootstrap/contract/SecretsClassificationRegistryTest.java신규 test (registry↔상수 drift, snakeyaml + assumeTrueSKIP)RepositoryAccessCapabilityRegistryTest/ErrorCodeRegistryMappingTestC3 CleanArchitectureTest.java(+1@ArchTest no_refresh_scope_anywhere, FQN stringbeAnnotatedWith)기존 파일 수정 기존 noClasses()ArchRuleC3 .../architecture/violations/secrets/RefreshScopeUsingFixture.java신규 test fixture ( @RefreshScope)SpringWebSocketHandlerFixtureC3 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 testCompileOnlyfixture 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 그대로 적용:
SecretSourceValidator1-arg ctor(ConfigurableEnvironment만),REQUIRED_PROD_SECRETS= registryclassification: secret+prod_default: null6 key 와 C2 가 1:1 단언(drift 시 build fail),@RefreshScope전면 금지(carve-out 없음, FQN 문자열 참조).REQUIRED_PROD_SECRETS만public(C2 가 cross-package…contract에서 읽어야 하므로 —StartupSafetyValidator.REQUIRED_MULTI_INSTANCE_BEANS의 package-private 와 다른 의도적 차이).검증:
./gradlew :app-bootstrap:test(21 class 전체 PASS, 0 skip —docs/존재 시 C2 drift 단언 실측 통과) +verifyCleanArchitectureDependenciesPASS. 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 가드. snakeyamlYaml로docs/registries/secrets-classification.yaml로드 →classification: secret+prod_default: nullrow 집합이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) + wiringRuntimeSafetyConfig.java.
- 메커니즘 PIN =
SmartInitializingSingleton(이전EnvironmentPostProcessor후보를 폐기). 근거: ca-tmpl 의 시작 검증이 이미StartupSafetyValidator로SmartInitializingSingleton에 통일돼 있고(그 Javadoc 이 EPP/ApplicationReadyEvent 대비 timing 근거를 명시), 본 검증도 같은 prod-profile + Environment 값 검사 부류 → 동일 메커니즘이 일관적.- 잔여 trade-off(명시):
SmartInitializingSingleton은 singleton 인스턴스화 후 실행 → eagerDataSource가__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-argEnvironment + RuntimeSafetySettings + ListableBeanFactory이나, 본 검사는 bean-presence 조회 불요·RuntimeSafetySettings미사용·Environment property 값만 필요 → 의도적 단순화).afterSingletonsInstantiated()가 아래 두 검사 호출:validateNoLocalDevSentinelInProd(): prod active 시environment.getPropertySources()의 각EnumerablePropertySource값 스캔 →__LOCAL_DEV_로 시작하는 값 발견 시 위반 key 나열한IllegalStateExceptionthrow(context refresh 중단).validateRequiredSecretsPresent(): prod active 시 in-code 상수REQUIRED_PROD_SECRETS(= registryclassification: secret+prod_default: nullkey 목록;StartupSafetyValidator.REQUIRED_MULTI_INSTANCE_BEANS와 동일한 상수 패턴) 의 각 keyenvironment.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 은 진짜 대안)인데 포트가 없어 registrysource:텍스트가 죽은 분류값이었음. →SecretSource포트 +EnvironmentSecretSource기본 +SecretSourceStrategyenum +SecretSourceFactory+SecretSourceProperties선박.SecretSourceValidator의 required-secret 검사가 이제 포트(secretSource.resolve(key))를 경유 → backend 스왑을 따라감.
요소 클래스 비고 포트 SecretSource(Optional<String> resolve(key))blank=absent 강제 기본 구현 EnvironmentSecretSourceSpring 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_MANAGERstrategy 는 enum 주석 + factory 확장점으로 예약(미선박). registry per-rowsource:는 분류 메타로 잔존(글로벌 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전체@RefreshScope0건(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_anywhereArchRule:@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 위험 —ArchitectureViolationFixtureTest의SPRING_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). 값은 registryrotation_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 는 registryrotation_policyenum 값 등록만, 실제 rotation 메커니즘/주기 근거는 owner branch.UNSUPPORTED_IMPL_DECISION:
dual-bindwindow 값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·/envmasking 강제 지점 →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: nullkey 가 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-C5default 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 도달 전 차단.
- required secret 누락 (prod):
- 다른 계약 의존:
- raw/branch-notes/feature-security-operational-baseline — JWT signing key
overlap-24hrotation 정책 consume(본 branch 는APP_SECURITY_JWT_SIGNING_KEY저장/주입/분류만). 그 값이 바뀌면 registry row 동기화 필요(D8). - raw/branch-notes/feature-management-actuator-security-contract — 본 branch
masking_rule분류를 actuator/configprops·/env노출 지점에서 강제.configprops는 prod forbidden 이 1차 방어. - raw/branch-notes/feature-log-management-contract — log masking converter 가 secret value 의 실제 출력 마스킹 강제(본 branch 는 분류만 제공).
- raw/branch-notes/feature-data-retention-privacy-contract —
APP_PRIVACY_PSEUDONYMIZATION_SALT의salt-rotation-90d소유(registryowner_branch). 본 branch 는 tier(secret) 분류만. - raw/branch-notes/feature-contract-registry-governance —
secrets-classification.yamlschema 소유(Schema owner주석). 본 branch 는 row 추가, schema/검증 규칙은 그쪽. - raw/branch-notes/feature-env-driven-runtime-configuration —
public-configtier(APP_PROFILE등)는env-keys.yaml소유. 본 branch 는secret/sensitive-config만 분류, public 은 reference row.- 이중 분류 충돌 해소 규칙(F5):
APP_DATASOURCE_URL은env-keys.yaml에서public-config,secrets-classification.yaml에서sensitive-config로 두 번 등장한다. 우선순위 = 노출 통제 관점이 항상 우선 — masking/노출 강제 로직(actuator·log)은secrets-classification.yaml의 tier(sensitive-config→full_except_last_4)를 읽고,env-keys.yaml의public-config는 값 존재·default·reload 정책 메타에만 적용. 두 registry 의 schema 일관성은 raw/branch-notes/feature-contract-registry-governance 가 보증.
- 이중 분류 충돌 해소 규칙(F5):
- raw/branch-notes/feature-security-operational-baseline — JWT signing key
테스트 계약
- prod profile에서 secret이 config dump/log에 노출되면 실패.
- required secret 누락 시 startup이 성공하면 실패.
- local-only
.env설정이 prod에서 허용되면 실패. - masking 없는 secret value 출력은 실패.
- secret reload 검증: 결정 사항에 따라 secret reload는
no-runtime-reload(재시작 강제). 측정 방법: contract testSecretReloadContractTest에서 secret manager의 secret value 변경 후 application이 자동 reload하지 않음 verify.@RefreshScopebean 등록 시 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-secrets 의 Secrets / 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):
@RefreshScopefixture 가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 가 다른 패키지에 있기 때문.
묶음
- raw/company-tech-blogs/secrets-1password-developer-secret-references
- raw/official-docs/config-12-factor-app-config
- raw/official-docs/config-aws-appconfig-feature-flag-deployment
- raw/official-docs/config-spring-cloud-config-server-official
- raw/official-docs/config-spring-cloud-kubernetes-configmap-reload
- raw/official-docs/secrets-aws-secrets-manager-rotation
- raw/official-docs/secrets-k8s-secret-external-secrets-operator
- raw/official-docs/secrets-vault-dynamic-secrets-hashicorp
본 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+verifyCleanArchitectureDependenciesPASS. 미커밋(사용자 커밋 예정).
완료 후 정리
- PR 링크:
- 리뷰 메모:
- 머지 결과 / 배포 환경:
- wiki 추출 대상 (verified만,
wiki/projects/로만 추출):actually-implemented항목: registrysecrets-classification.yamllocally-verified항목: C1SecretSourceValidator/SecretSourceConfig/SecretSourceValidatorTest, C2SecretsClassificationRegistryTest, C3no_refresh_scope_anywhereArchRule +RefreshScopeUsingFixture+ fixture 검증 테스트, §4SecretReloadContractTest(선택 보강도 구현), D5SecretSource포트 +EnvironmentSecretSource기본 +SecretSourceStrategy/SecretSourceFactory/SecretSourceProperties+SecretSourceTest(2026-06-09 추상화 승급;:app-bootstrap:test140/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 실측 필요, 코드 산출물 아님)