--- id: devops-secrets-rotation-automation title: Secrets Rotation — 자동 회전 / 무중단 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [devops, secrets, rotation, security, vibe-coding] tech_stack: { language: "TS / AWS / Vault", applicable_to: ["DevOps"] } applied_in: [] aliases: [secret rotation, AWS Secrets Manager, HashiCorp Vault, KMS, key rotation] --- # Secrets Rotation > Secret 영원히 같으면 leak 시 영원히 노출. **자동 회전 + 무중단 (dual-secret window)**. AWS Secrets Manager / HashiCorp Vault / Kubernetes External Secrets. ## 📖 핵심 개념 - Static secret: 수동 회전 — 잊혀짐. - Dynamic secret: 매 요청마다 발급 (Vault dynamic creds). - Dual-secret window: 새 secret 활성, 기존도 N분 유효. - Lease: 짧은 시간만 유효, 갱신 필요. ## 💻 코드 패턴 ### AWS Secrets Manager rotation ```ts // Lambda rotation function (4-step) export const handler = async (event: RotationEvent) => { const { Step, SecretId, ClientRequestToken } = event; switch (Step) { case 'createSecret': return createSecret(SecretId, ClientRequestToken); case 'setSecret': return setSecret(SecretId, ClientRequestToken); // DB 에 새 password 적용 case 'testSecret': return testSecret(SecretId, ClientRequestToken); // 새 password 로 연결 확인 case 'finishSecret': return finishSecret(SecretId, ClientRequestToken); // AWSCURRENT 로 promote } }; ``` ```hcl # Terraform resource "aws_secretsmanager_secret_rotation" "db" { secret_id = aws_secretsmanager_secret.db.id rotation_lambda_arn = aws_lambda_function.rotate.arn rotation_rules { automatically_after_days = 30 } } ``` ### App 측 — refresh 주기 ```ts class SecretCache { private cached: { value: string; fetchedAt: number } | null = null; private ttlMs = 60_000; async get(): Promise { if (!this.cached || Date.now() - this.cached.fetchedAt > this.ttlMs) { const v = await fetchFromSecretsManager(this.secretId); this.cached = { value: v, fetchedAt: Date.now() }; } return this.cached.value; } } ``` ### Dual-credential window ```sql -- DB 에 user app_v1, app_v2 둘 다 존재 -- v1 active 동안 v2 만들고 → v2 로 새 deploy → v1 비활성 CREATE USER app_v2 WITH PASSWORD 'new'; GRANT ALL ON DATABASE app TO app_v2; -- 새 pod 들 v2 사용 시작 -- 1시간 후 DROP USER app_v1; ``` ### Vault dynamic creds ```ts // app 이 매번 짧은 lease 의 creds 받음 const r = await vault.read('database/creds/readonly'); const { username, password, lease_id, lease_duration } = r.data; setTimeout(() => vault.renew(lease_id), lease_duration * 0.7 * 1000); ``` ### Kubernetes External Secrets ```yaml apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: { name: db-secret } spec: refreshInterval: 1h secretStoreRef: { name: aws-secrets, kind: ClusterSecretStore } target: { name: db, creationPolicy: Owner } data: - secretKey: url remoteRef: { key: app/db, property: url } ``` ESO 가 Secret 자동 sync — 회전되면 1h 안에 pod 에 반영. Pod restart 또는 reloader 로 새 값 로드. ### App restart on secret change ```yaml # stakater/reloader 추가 metadata: annotations: reloader.stakater.com/auto: "true" ``` ### KMS key rotation ```hcl resource "aws_kms_key" "main" { description = "App data" deletion_window_in_days = 30 enable_key_rotation = true # 매년 자동 회전 } ``` ### API key 회전 패턴 ```ts // 사용자 API key — 새거 발급 시 24h 둘 다 활성 async function rotateApiKey(userId: string): Promise { const old = await db.apiKeys.find(userId); const newKey = generate(); await db.apiKeys.insert({ userId, key: newKey, status: 'active' }); await db.apiKeys.update(old.id, { status: 'sunset', expiresAt: now() + 24 * H }); return newKey; } ``` ## 🤔 의사결정 기준 | 종류 | 솔루션 | |---|---| | Cloud (AWS) | Secrets Manager + 자동 rotation lambda | | Cloud (GCP) | Secret Manager + Cloud Functions | | K8s | External Secrets Operator | | Self-hosted | HashiCorp Vault | | Static creds 만 | Doppler / 1Password Connect | | Dynamic / short-lived | Vault dynamic secrets | | Key encryption | KMS / Cloud KMS / Vault Transit | ## ❌ 안티패턴 - **Rotation 수동**: 잊혀짐. 자동. - **새 secret 즉시 강제 — old 비활성**: 아직 transition 중인 pod 다운. - **Secret env var 만 — restart 필요**: ESO + Reloader. - **Repo 에 commit (`.env.prod`)**: leak. .gitignore + secret scan. - **Logging 시 secret 출력**: 마스킹. - **단일 user 모든 service 공유**: 한 leak = 전체. - **회전 불가능한 client (mobile app)**: refresh token 쓰고 짧은 access. - **Terraform state 안 secret 평문**: state encrypt + 권한 제한. ## 🤖 LLM 활용 힌트 - Secrets Manager / Vault + 자동 rotation lambda. - App = 짧은 cache + 회전 가능 구조. - ESO + Reloader = K8s 표준. ## 🔗 관련 문서 - [[DevOps_Terraform_Patterns]]