--- title: branch / feature-secrets-config-source-contract source_type: branch-note status: raw branch: feature-secrets-config-source-contract parent_branch: related_projects: [ca-skeleton] governing_docs: [wiki/projects/ca-tmpl/security-baseline-jwt-actuator-secrets.md] tags: [branch, ca-skeleton, secrets, config] created: 2026-05-22 target_merge: status_label: in-progress id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-020 kind: project-work-item project: ca-skeleton-operational-contract work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-020 inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1] refines: [] overrides: [] depends_on: [] contract_packet: 1 contract_packet_sha256: 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 명시적 거부 - **비교 핵심**: 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 는 `@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 구현 완료).** 아래 C1~C3 모두 코드 작성 + 테스트 통과. `actually-implemented` 표기 항목은 registry yaml + C1~C3 산출물. > > ### 구현 결과 (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_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 단언 실측 통과) + `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 `Yaml` 로 `docs/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 의 시작 검증이 이미 `StartupSafetyValidator` 로 `SmartInitializingSingleton` 에 통일돼 있고(그 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("")` (= `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 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 위험 — `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**). 값은 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 도달 전 차단. - **다른 계약 의존**: - [[raw/branch-notes/feature-security-operational-baseline]] — JWT signing key `overlap-24h` rotation 정책 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` 소유(registry `owner_branch`). 본 branch 는 tier(secret) 분류만. - [[raw/branch-notes/feature-contract-registry-governance]] — `secrets-classification.yaml` *schema* 소유(`Schema owner` 주석). 본 branch 는 row 추가, schema/검증 규칙은 그쪽. - [[raw/branch-notes/feature-env-driven-runtime-configuration]] — `public-config` tier(`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]] 가 보증. ## 테스트 계약 - 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-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)**: `@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 가 다른 패키지에 있기 때문. ## 묶음 - [[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]] - [[raw/blog-topics/secret-source-port-restart-only-rotation-2026-07-02]] > 본 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 실측 필요, 코드 산출물 아님)