Files
2nd/10_Wiki/Topics/Coding/DevOps_Vault_Secrets.md
T
2026-05-10 22:08:15 +09:00

9.3 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
devops-vault-secrets Vault — secrets management deep Coding draft B conceptual 2026-05-09 2026-05-09
devops
security
vault
vibe-coding
language applicable_to
HCL / CLI
DevOps
Backend
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)

# Dev mode (memory)
vault server -dev

# Production
vault server -config=/etc/vault/config.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)

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)

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

# 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
# Application 가 호출
vault read database/creds/readonly
# username = v-readonly-abc123
# password = ...

→ 매번 새 DB user. 1 시간 유효. 자동 정리.

AppRole auth

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 = ...
# Login
vault write auth/approle/login \
    role_id=... \
    secret_id=...
# token = ...

→ App 가 role_id + secret_id 로 vault token 발급. Short TTL.

Kubernetes auth

# Service account
apiVersion: v1
kind: ServiceAccount
metadata:
  name: myapp
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
// 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)

# myapp-policy.hcl
path "secret/data/myapp/*" {
  capabilities = ["read"]
}

path "database/creds/readonly" {
  capabilities = ["read"]
}

path "transit/encrypt/myapp" {
  capabilities = ["update"]
}
vault policy write myapp-policy myapp-policy.hcl

Transit (encrypt-as-a-service)

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 보유.

// 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)

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

// 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)

# 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)

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

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)

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
# Vault transit 가 key rotation 자동
vault write -f transit/keys/myapp/rotate
# 옛 ciphertext 도 decrypt 가능 (auto upgrade)

External Secrets Operator (K8s)

# 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

vault audit enable file file_path=/var/log/vault/audit.log

→ 매 access = log. Compliance.

Backup / DR

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 항상.

🔗 관련 문서