[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,411 @@
|
||||
---
|
||||
id: devops-vault-secrets
|
||||
title: Vault — secrets management deep
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [devops, security, vault, vibe-coding]
|
||||
tech_stack: { language: "HCL / CLI", applicable_to: ["DevOps", "Backend"] }
|
||||
applied_in: []
|
||||
aliases: [HashiCorp Vault, secrets, dynamic credentials, KV store, transit, PKI, AppRole]
|
||||
---
|
||||
|
||||
# Vault Secrets Management
|
||||
|
||||
> Secret 가 git / env file 가 답 X. **Vault: 동적 credential, encryption-as-a-service, PKI, audit**. AWS Secrets Manager / GCP Secret Manager 가 cloud-managed.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- Static secret: API key, DB password.
|
||||
- Dynamic secret: 매번 새 (DB 가짜 user).
|
||||
- Encryption: encrypt-as-a-service.
|
||||
- Auth method: how to access vault.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### Vault setup (dev)
|
||||
```bash
|
||||
# Dev mode (memory)
|
||||
vault server -dev
|
||||
|
||||
# Production
|
||||
vault server -config=/etc/vault/config.hcl
|
||||
```
|
||||
|
||||
```hcl
|
||||
# config.hcl
|
||||
storage "raft" {
|
||||
path = "/vault/data"
|
||||
node_id = "node1"
|
||||
}
|
||||
|
||||
listener "tcp" {
|
||||
address = "0.0.0.0:8200"
|
||||
tls_cert_file = "/etc/vault/cert.pem"
|
||||
tls_key_file = "/etc/vault/key.pem"
|
||||
}
|
||||
|
||||
api_addr = "https://vault.example.com:8200"
|
||||
```
|
||||
|
||||
### KV (key-value)
|
||||
```bash
|
||||
vault kv put secret/myapp/db password='my-secret'
|
||||
vault kv get secret/myapp/db
|
||||
# password = my-secret
|
||||
|
||||
vault kv get -field=password secret/myapp/db
|
||||
# my-secret
|
||||
```
|
||||
|
||||
### KV v2 (versioned)
|
||||
```bash
|
||||
vault kv put secret/myapp/db password='v2-secret'
|
||||
vault kv get -version=1 secret/myapp/db # 옛 version
|
||||
vault kv rollback -version=1 secret/myapp/db
|
||||
```
|
||||
|
||||
### Dynamic DB credential
|
||||
```bash
|
||||
# Setup
|
||||
vault secrets enable database
|
||||
|
||||
vault write database/config/postgres \
|
||||
plugin_name=postgresql-database-plugin \
|
||||
allowed_roles=readonly \
|
||||
connection_url='postgresql://{{username}}:{{password}}@db:5432/app' \
|
||||
username='vault-admin' \
|
||||
password='admin-pw'
|
||||
|
||||
vault write database/roles/readonly \
|
||||
db_name=postgres \
|
||||
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
|
||||
default_ttl=1h \
|
||||
max_ttl=24h
|
||||
```
|
||||
|
||||
```bash
|
||||
# Application 가 호출
|
||||
vault read database/creds/readonly
|
||||
# username = v-readonly-abc123
|
||||
# password = ...
|
||||
```
|
||||
|
||||
→ 매번 새 DB user. 1 시간 유효. 자동 정리.
|
||||
|
||||
### AppRole auth
|
||||
```bash
|
||||
vault auth enable approle
|
||||
|
||||
vault write auth/approle/role/myapp \
|
||||
secret_id_ttl=10m \
|
||||
token_ttl=1h \
|
||||
token_max_ttl=24h \
|
||||
policies=myapp-policy
|
||||
|
||||
# Get role + secret
|
||||
vault read auth/approle/role/myapp/role-id
|
||||
# role_id = ...
|
||||
vault write -f auth/approle/role/myapp/secret-id
|
||||
# secret_id = ...
|
||||
```
|
||||
|
||||
```bash
|
||||
# Login
|
||||
vault write auth/approle/login \
|
||||
role_id=... \
|
||||
secret_id=...
|
||||
# token = ...
|
||||
```
|
||||
|
||||
→ App 가 role_id + secret_id 로 vault token 발급. Short TTL.
|
||||
|
||||
### Kubernetes auth
|
||||
```yaml
|
||||
# Service account
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: myapp
|
||||
```
|
||||
|
||||
```bash
|
||||
vault auth enable kubernetes
|
||||
vault write auth/kubernetes/role/myapp \
|
||||
bound_service_account_names=myapp \
|
||||
bound_service_account_namespaces=default \
|
||||
policies=myapp-policy \
|
||||
ttl=1h
|
||||
```
|
||||
|
||||
```ts
|
||||
// Pod 안
|
||||
const jwt = readFileSync('/var/run/secrets/kubernetes.io/serviceaccount/token', 'utf8');
|
||||
const r = await fetch('https://vault:8200/v1/auth/kubernetes/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ role: 'myapp', jwt }),
|
||||
});
|
||||
const token = (await r.json()).auth.client_token;
|
||||
```
|
||||
|
||||
### Policy (HCL)
|
||||
```hcl
|
||||
# myapp-policy.hcl
|
||||
path "secret/data/myapp/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "database/creds/readonly" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
path "transit/encrypt/myapp" {
|
||||
capabilities = ["update"]
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
vault policy write myapp-policy myapp-policy.hcl
|
||||
```
|
||||
|
||||
### Transit (encrypt-as-a-service)
|
||||
```bash
|
||||
vault secrets enable transit
|
||||
vault write -f transit/keys/myapp
|
||||
|
||||
# Encrypt
|
||||
vault write transit/encrypt/myapp plaintext=$(echo "secret data" | base64)
|
||||
# ciphertext = vault:v1:abcdef...
|
||||
|
||||
# Decrypt
|
||||
vault write transit/decrypt/myapp ciphertext=vault:v1:abcdef...
|
||||
```
|
||||
|
||||
→ App 가 plaintext 가짐 적게. Vault 가 key 보유.
|
||||
|
||||
```ts
|
||||
// Postgres column 암호화
|
||||
async function encryptField(plain: string) {
|
||||
const r = await vault.write('transit/encrypt/myapp', {
|
||||
plaintext: Buffer.from(plain).toString('base64'),
|
||||
});
|
||||
return r.data.ciphertext;
|
||||
}
|
||||
|
||||
async function decryptField(cipher: string) {
|
||||
const r = await vault.write('transit/decrypt/myapp', { ciphertext: cipher });
|
||||
return Buffer.from(r.data.plaintext, 'base64').toString('utf8');
|
||||
}
|
||||
```
|
||||
|
||||
### PKI (Certificate Authority)
|
||||
```bash
|
||||
vault secrets enable pki
|
||||
vault write pki/root/generate/internal \
|
||||
common_name=example.com ttl=87600h
|
||||
|
||||
vault write pki/roles/myapp \
|
||||
allowed_domains=example.com \
|
||||
allow_subdomains=true \
|
||||
max_ttl=720h
|
||||
|
||||
vault write pki/issue/myapp common_name=app.example.com ttl=24h
|
||||
# certificate = ...
|
||||
# private_key = ...
|
||||
```
|
||||
|
||||
→ Short-lived TLS cert.
|
||||
|
||||
### Lease & renewal
|
||||
```ts
|
||||
// Get secret
|
||||
const r = await vault.read('database/creds/readonly');
|
||||
const lease_id = r.lease_id;
|
||||
const ttl = r.lease_duration; // 3600
|
||||
|
||||
// 자동 renew
|
||||
setInterval(() => vault.renew(lease_id), ttl * 0.5 * 1000);
|
||||
|
||||
// 또는 expire 후 다시 read
|
||||
```
|
||||
|
||||
### Vault Agent (sidecar)
|
||||
```hcl
|
||||
# vault-agent.hcl
|
||||
auto_auth {
|
||||
method "approle" {
|
||||
config = { role_id_file_path = "/etc/vault/role_id" }
|
||||
}
|
||||
}
|
||||
|
||||
template {
|
||||
source = "/etc/vault/db.tpl"
|
||||
destination = "/etc/myapp/db.yml"
|
||||
}
|
||||
```
|
||||
|
||||
→ Agent 가 vault 접근, app 가 file 만 읽음.
|
||||
|
||||
### AWS Secrets Manager (managed)
|
||||
```ts
|
||||
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
|
||||
const client = new SecretsManagerClient({ region: 'us-east-1' });
|
||||
|
||||
const r = await client.send(new GetSecretValueCommand({ SecretId: 'myapp/db' }));
|
||||
const secret = JSON.parse(r.SecretString!);
|
||||
```
|
||||
|
||||
→ 자동 IAM. Lambda / ECS 친화. Rotation 옵션.
|
||||
|
||||
### GCP Secret Manager
|
||||
```ts
|
||||
import { SecretManagerServiceClient } from '@google-cloud/secret-manager';
|
||||
const client = new SecretManagerServiceClient();
|
||||
|
||||
const [version] = await client.accessSecretVersion({
|
||||
name: 'projects/my-proj/secrets/db-pw/versions/latest',
|
||||
});
|
||||
const secret = version.payload!.data!.toString();
|
||||
```
|
||||
|
||||
### Doppler / Infisical (cross-cloud)
|
||||
```bash
|
||||
doppler login
|
||||
doppler run -- node app.js
|
||||
# → env var 자동 주입
|
||||
```
|
||||
|
||||
→ Vault 보다 simple. Multi-cloud.
|
||||
|
||||
### .env file (only dev)
|
||||
```
|
||||
# .env (gitignore)
|
||||
DATABASE_URL=postgres://...
|
||||
API_KEY=...
|
||||
|
||||
# 로컬 개발 만. Production 에 X.
|
||||
```
|
||||
|
||||
→ git 에 commit X. `.env.example` 만.
|
||||
|
||||
### Rotation
|
||||
```
|
||||
- DB password: 매 7-30 days 자동
|
||||
- API key: 매 90 days
|
||||
- TLS cert: 매 90 days (Let's Encrypt 식)
|
||||
- Encryption key: 매 1 year + re-encrypt
|
||||
```
|
||||
|
||||
```bash
|
||||
# Vault transit 가 key rotation 자동
|
||||
vault write -f transit/keys/myapp/rotate
|
||||
# 옛 ciphertext 도 decrypt 가능 (auto upgrade)
|
||||
```
|
||||
|
||||
### External Secrets Operator (K8s)
|
||||
```yaml
|
||||
# K8s 가 vault → secret 자동 sync
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: db-secret
|
||||
spec:
|
||||
refreshInterval: 1m
|
||||
secretStoreRef:
|
||||
name: vault-store
|
||||
target:
|
||||
name: db-secret # K8s Secret
|
||||
data:
|
||||
- secretKey: password
|
||||
remoteRef:
|
||||
key: secret/myapp/db
|
||||
property: password
|
||||
```
|
||||
|
||||
→ App 가 K8s secret 만, vault 직접 안 호출.
|
||||
|
||||
### Audit log
|
||||
```bash
|
||||
vault audit enable file file_path=/var/log/vault/audit.log
|
||||
```
|
||||
|
||||
→ 매 access = log. Compliance.
|
||||
|
||||
### Backup / DR
|
||||
```bash
|
||||
vault operator raft snapshot save snapshot.bak
|
||||
vault operator raft snapshot restore snapshot.bak
|
||||
```
|
||||
|
||||
### Cost
|
||||
```
|
||||
Vault OSS: 무료 (self-host).
|
||||
Vault Enterprise: $$$ (HSM, performance replication).
|
||||
HCP Vault: managed, $$.
|
||||
AWS Secrets Manager: $0.40/secret/month + $0.05/10k requests.
|
||||
GCP Secret Manager: $0.06/secret/version/month.
|
||||
```
|
||||
|
||||
### When?
|
||||
```
|
||||
- 큰 system: Vault.
|
||||
- AWS-only / 작음: Secrets Manager.
|
||||
- GCP-only: GCP SM.
|
||||
- Multi-cloud: Doppler / Infisical.
|
||||
- 작은 팀 / 작은 secrets: Doppler.
|
||||
```
|
||||
|
||||
### Seal / unseal
|
||||
```
|
||||
Vault start = sealed (data encrypted).
|
||||
Unseal = key share (Shamir secret sharing).
|
||||
- 5 keys total, 3 needed to unseal.
|
||||
|
||||
→ HA / cloud auto-unseal: KMS (AWS / GCP / Azure).
|
||||
```
|
||||
|
||||
### 함정
|
||||
```
|
||||
- Root token in env: 영구 access.
|
||||
- TTL 무한: 안 expire.
|
||||
- AppRole secret_id 누설: 누구나 access.
|
||||
- Audit log 안 봄: 침해 모름.
|
||||
- Backup 안 함: data lost.
|
||||
- Single-node: SPOF.
|
||||
- Performance replica + write 불가 무시.
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| 큰 / multi-region | Vault (Raft + replica) |
|
||||
| AWS only | Secrets Manager |
|
||||
| GCP only | GCP Secret Manager |
|
||||
| 작은 / multi-cloud | Doppler / Infisical |
|
||||
| K8s | External Secrets + Vault |
|
||||
| Encryption | Vault transit |
|
||||
| TLS | Vault PKI |
|
||||
| Dynamic DB cred | Vault DB plugin |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **.env in git**: 누설.
|
||||
- **Long-lived static secret**: rotation 없음 = exposure.
|
||||
- **Root token in app**: principle of least privilege 위반.
|
||||
- **No audit log**: blind.
|
||||
- **모든 secret 1 path**: 권한 분리 X.
|
||||
- **No backup**: lost.
|
||||
- **Hardcoded vault address**: failover 못 함.
|
||||
- **TTL 무한**: forever access.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- Vault = enterprise. AWS / GCP SM = cloud-managed.
|
||||
- Dynamic credential 가 가장 큰 가치.
|
||||
- Transit = encryption-as-a-service.
|
||||
- TTL + rotation 항상.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Security_Secrets_Management]]
|
||||
- [[DevOps_Secrets_Rotation_Automation]]
|
||||
- [[Security_Zero_Trust]]
|
||||
Reference in New Issue
Block a user