[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
---
|
||||
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]]
|
||||
- [[Backend_API_Auth_Strategies]]
|
||||
- [[Security_Encryption_at_Rest]]
|
||||
Reference in New Issue
Block a user