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

411 lines
45 KiB
Markdown

---
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 기준을 정의합니다.
<!-- section-id: branch-parent -->
## 부모 (필수)
- **Parent project (canonical SSOT)**: [[raw/project-notes/ca-skeleton-operational-contract]]
> ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: secret source·classification·leakage negative test가 통과한다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-STACK-FRAMEWORK-001@1` | framework는 Spring Boot 3.5.14다 | Work Item 완료 조건에 적용 | [[raw/project-notes/ca-skeleton-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
env-driven configuration만으로는 secret 관리 기준이 부족합니다. local `.env`, prod secret source, config dump 금지, rotation 고려를 skeleton 계약에 포함해야 합니다.
- 이슈:
- PR:
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- 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("<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 위험 — `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 가 다른 패키지에 있기 때문.
## 묶음
<!-- GENERATED: sources:start -->
- [[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]]
<!-- GENERATED: sources:end -->
<!-- GENERATED: blog-topics:start -->
- [[raw/blog-topics/secret-source-port-restart-only-rotation-2026-07-02]]
<!-- GENERATED: blog-topics:end -->
> 본 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 실측 필요, 코드 산출물 아님)