---
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<string> {
    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<string> {
  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]]
