# Vault 예시 Vault 1.17+ + Helm chart `hashicorp/vault` + VSO 0.8+ 기준. 모든 manifest는 `kubectl apply` 적용 가능한 완전한 형태다. --- ## 좋은 예시 1: Helm values.yaml — HA Raft + auto-unseal + audit ```yaml # values/vault-prod.yaml global: enabled: true tlsDisable: false injector: enabled: true replicas: 2 resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 256Mi server: image: repository: hashicorp/vault tag: "1.17.6" resources: requests: cpu: 250m memory: 256Mi limits: cpu: "1" memory: 512Mi extraEnvironmentVars: VAULT_CACERT: /vault/tls/ca.crt VAULT_TLSCERT: /vault/tls/tls.crt VAULT_TLSKEY: /vault/tls/tls.key AWS_REGION: ap-northeast-2 volumes: - name: vault-tls secret: secretName: vault-tls volumeMounts: - name: vault-tls mountPath: /vault/tls readOnly: true serviceAccount: create: true name: vault annotations: eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/vault-autounseal readinessProbe: enabled: true path: "/v1/sys/health?standbyok=true&perfstandbyok=true&uninitcode=204" port: 8200 scheme: HTTPS failureThreshold: 2 initialDelaySeconds: 5 periodSeconds: 5 timeoutSeconds: 3 livenessProbe: enabled: true path: "/v1/sys/health?standbyok=true&sealedcode=204&uninitcode=204" port: 8200 scheme: HTTPS failureThreshold: 3 initialDelaySeconds: 60 periodSeconds: 30 timeoutSeconds: 3 dataStorage: enabled: true size: 20Gi storageClass: ebs-gp3 accessMode: ReadWriteOnce mountPath: /vault/data auditStorage: enabled: true size: 10Gi storageClass: ebs-gp3 accessMode: ReadWriteOnce mountPath: /vault/audit service: enabled: true type: ClusterIP port: 8200 targetPort: 8200 ha: enabled: true replicas: 3 apiAddr: "https://$(POD_IP):8200" clusterAddr: "https://$(HOSTNAME).vault-internal:8201" raft: enabled: true setNodeId: true config: | ui = true listener "tcp" { address = "[::]:8200" cluster_address = "[::]:8201" tls_cert_file = "/vault/tls/tls.crt" tls_key_file = "/vault/tls/tls.key" tls_min_version = "tls13" } storage "raft" { path = "/vault/data" retry_join { leader_api_addr = "https://vault-0.vault-internal:8200" leader_ca_cert_file = "/vault/tls/ca.crt" leader_client_cert_file = "/vault/tls/tls.crt" leader_client_key_file = "/vault/tls/tls.key" } retry_join { leader_api_addr = "https://vault-1.vault-internal:8200" leader_ca_cert_file = "/vault/tls/ca.crt" leader_client_cert_file = "/vault/tls/tls.crt" leader_client_key_file = "/vault/tls/tls.key" } retry_join { leader_api_addr = "https://vault-2.vault-internal:8200" leader_ca_cert_file = "/vault/tls/ca.crt" leader_client_cert_file = "/vault/tls/tls.crt" leader_client_key_file = "/vault/tls/tls.key" } } seal "awskms" { region = "ap-northeast-2" kms_key_id = "alias/vault-autounseal" } service_registration "kubernetes" {} telemetry { prometheus_retention_time = "24h" disable_hostname = true } affinity: | podAntiAffinity: requiredDuringSchedulingIgnoredDuringExecution: - labelSelector: matchLabels: app.kubernetes.io/name: vault component: server topologyKey: kubernetes.io/hostname topologySpreadConstraints: | - maxSkew: 1 topologyKey: topology.kubernetes.io/zone whenUnsatisfiable: ScheduleAnyway labelSelector: matchLabels: app.kubernetes.io/name: vault component: server ``` **왜 좋은가:** - `ha.enabled=true` + `raft.enabled=true` + `raft.setNodeId=true` 3종 필수 플래그 - listener와 storage raft stanza가 8200/8201 모두 바인드, `cluster_address` 명시 → peer replication 성립 - `seal "awskms"`로 auto-unseal, Pod 재시작 시 수동 개입 불필요 - `auditStorage.enabled=true` → audit 전용 PVC 분리 (dataStorage 오염 방지) - IRSA(`eks.amazonaws.com/role-arn`)로 KMS 접근 권한 위임 (static IAM key 없음) --- ## 나쁜 예시 1: chart 기본값 standalone ```bash helm install vault hashicorp/vault --namespace vault --create-namespace ``` **문제:** - 기본은 `standalone` + `file` storage → single pod, PVC 1개, HA 없음, snapshot restore로만 복구 - Shamir 수동 unseal → pod 재시작마다 운영자 개입 - audit device 미활성 → 감사 로그 없음 - chart 문서 자체가 "not suitable for production"이라 명시 --- ## 좋은 예시 2: Service — 8200 + 8201 둘 다 expose Helm chart가 자동 생성하지만, 수제 Service 예시: ```yaml --- apiVersion: v1 kind: Service metadata: name: vault namespace: vault labels: app.kubernetes.io/name: vault app.kubernetes.io/instance: vault-prod spec: type: ClusterIP selector: app.kubernetes.io/name: vault component: server ports: - name: https port: 8200 targetPort: 8200 protocol: TCP --- apiVersion: v1 kind: Service metadata: name: vault-internal namespace: vault labels: app.kubernetes.io/name: vault spec: type: ClusterIP clusterIP: None publishNotReadyAddresses: true selector: app.kubernetes.io/name: vault component: server ports: - name: https port: 8200 targetPort: 8200 - name: https-internal port: 8201 targetPort: 8201 ``` **왜 좋은가:** - `vault-internal` headless + `publishNotReadyAddresses: true` → Raft peer가 unseal 전에도 서로 발견 가능 - **8201 포트 expose** → peer-to-peer Raft replication 성립 (누락 시 leader election 영구 실패) - 사용자용 `vault` Service는 8200만 노출 --- ## 나쁜 예시 2: 8201 누락 ```yaml spec: ports: - port: 8200 targetPort: 8200 ``` **문제:** - Raft peer가 8201로 서로 통신해야 하는데 Service가 expose하지 않음 - `vault operator raft list-peers`에서 follower가 리더로 못 붙음 - 증상: 단일 노드만 unsealed, 나머지는 "storage: IO error" 로그 루프 --- ## 좋은 예시 3: Kubernetes auth bootstrap + role ```bash # 1. Kubernetes auth method 활성화 vault auth enable kubernetes # 2. Vault가 Kubernetes TokenReview API를 호출하기 위한 설정 # (Vault Pod 내부에서 실행하거나 reviewer SA의 JWT를 주입) vault write auth/kubernetes/config \ token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \ kubernetes_host="https://kubernetes.default.svc.cluster.local" \ kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \ disable_iss_validation=false # 3. Policy 생성 (auth-server가 읽을 수 있는 경로만) vault policy write auth-server-read - <<'EOF' path "kv/data/auth-server/*" { capabilities = ["read"] } path "database/creds/auth-server" { capabilities = ["read"] } EOF # 4. Role 생성 — 특정 SA + namespace에만 바인딩 vault write auth/kubernetes/role/auth-server \ bound_service_account_names=auth-server \ bound_service_account_namespaces=auth-prod \ policies=auth-server-read \ ttl=1h \ max_ttl=24h \ audience=vault ``` **왜 좋은가:** - TokenReview JWT를 명시적으로 구성 → Vault가 SA 토큰 유효성 검증 가능 - Policy는 `kv/data/auth-server/*`, `database/creds/auth-server`만 허용 (최소 권한) - Role은 `auth-prod` namespace의 `auth-server` SA에만 바인딩 - `audience=vault`로 projected token의 audience 검증 (token confusion 방어) --- ## 나쁜 예시 3: wildcard role ```bash vault write auth/kubernetes/role/all-apps \ bound_service_account_names="*" \ bound_service_account_namespaces="*" \ policies=default \ ttl=720h ``` **문제:** - 모든 namespace의 모든 SA가 로그인 가능 → 한 워크로드 침해가 전체 Vault 접근으로 확대 - TTL 30일은 token revocation window가 너무 김 - `default` policy가 넓으면 실질적인 접근 제어 상실 --- ## 좋은 예시 4: VSO — 클러스터 수준 연결 + 앱 namespace auth ```yaml --- apiVersion: v1 kind: Namespace metadata: name: vault-secrets-operator-system --- # 1) 클러스터 전체 1개 VaultConnection apiVersion: secrets.hashicorp.com/v1beta1 kind: VaultConnection metadata: name: default namespace: vault-secrets-operator-system spec: address: https://vault.vault.svc.cluster.local:8200 tlsServerName: vault.vault.svc.cluster.local caCertSecretRef: vault-ca skipTLSVerify: false timeout: 60s --- # 2) 앱 namespace의 ServiceAccount apiVersion: v1 kind: ServiceAccount metadata: name: auth-server namespace: auth-prod --- # 3) 앱 namespace의 VaultAuth (Vault Kubernetes auth role로 로그인) apiVersion: secrets.hashicorp.com/v1beta1 kind: VaultAuth metadata: name: default namespace: auth-prod spec: vaultConnectionRef: vault-secrets-operator-system/default method: kubernetes mount: kubernetes kubernetes: role: auth-server serviceAccount: auth-server audiences: - vault tokenExpirationSeconds: 600 ``` **왜 좋은가:** - `VaultConnection` 1개를 operator namespace에 두고, 앱 namespace에서 cross-reference - `VaultAuth.method: kubernetes`가 Vault의 `auth/kubernetes/role/auth-server`를 호출 - `audiences: [vault]`로 projected SA token의 audience 바인딩 - `tokenExpirationSeconds: 600` → projected token 10분마다 rotate --- ## 좋은 예시 5: VSO — Static / Dynamic / PKI secret ```yaml --- # KV v2에서 정적 secret 동기화 apiVersion: secrets.hashicorp.com/v1beta1 kind: VaultStaticSecret metadata: name: auth-server-config namespace: auth-prod spec: vaultAuthRef: default mount: kv path: auth-server/config type: kv-v2 refreshAfter: 30m hmacSecretData: true rolloutRestartTargets: - kind: Deployment name: auth-server destination: name: auth-server-config create: true overwrite: true --- # Postgres dynamic credential (TTL 1h) apiVersion: secrets.hashicorp.com/v1beta1 kind: VaultDynamicSecret metadata: name: auth-server-db namespace: auth-prod spec: vaultAuthRef: default mount: database path: creds/auth-server renewalPercent: 67 rolloutRestartTargets: - kind: Deployment name: auth-server destination: name: auth-server-db create: true overwrite: true transformation: excludeRaw: true templates: DATABASE_URL: text: 'postgresql://{{ .Secrets.username }}:{{ .Secrets.password }}@auth-db-rw.auth-prod.svc.cluster.local:5432/authdb?sslmode=require' --- # PKI 인증서 발급 apiVersion: secrets.hashicorp.com/v1beta1 kind: VaultPKISecret metadata: name: auth-server-cert namespace: auth-prod spec: vaultAuthRef: default mount: pki_int role: auth-server commonName: auth-server.auth-prod.svc.cluster.local altNames: - auth-server - auth-server.auth-prod ipSans: [] ttl: 720h revoke: true clear: true expiryOffset: 120h destination: name: auth-server-cert create: true type: kubernetes.io/tls ``` **왜 좋은가:** - 세 패턴(정적 KV, 동적 DB credential, PKI cert)을 한 namespace에서 일관되게 선언 - `rolloutRestartTargets`로 secret 갱신 시 consumer Deployment 자동 롤링 재시작 - `renewalPercent: 67` → TTL 67% 경과 시 갱신 (default는 보통 70%) - `transformation.templates`로 연결 문자열 포맷 변환 (앱이 username/password 파싱 안 해도 됨) --- ## 좋은 예시 6: Vault Agent Injector (init-only 모드) ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: legacy-app namespace: legacy spec: replicas: 2 selector: matchLabels: app: legacy-app template: metadata: labels: app: legacy-app annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: "legacy-app" vault.hashicorp.com/agent-pre-populate-only: "true" vault.hashicorp.com/agent-inject-secret-db.env: "database/creds/legacy-app" vault.hashicorp.com/agent-inject-template-db.env: | {{ with secret "database/creds/legacy-app" -}} DATABASE_USERNAME={{ .Data.username }} DATABASE_PASSWORD={{ .Data.password }} {{- end }} vault.hashicorp.com/agent-inject-file-db.env: "db.env" vault.hashicorp.com/agent-limits-cpu: "200m" vault.hashicorp.com/agent-limits-mem: "128Mi" vault.hashicorp.com/agent-requests-cpu: "50m" vault.hashicorp.com/agent-requests-mem: "64Mi" spec: serviceAccountName: legacy-app securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 fsGroup: 10001 seccompProfile: type: RuntimeDefault containers: - name: app image: registry.example.com/legacy-app:1.4.2 command: ["sh", "-c", "source /vault/secrets/db.env && exec /app/run"] resources: requests: { cpu: 100m, memory: 128Mi } limits: { memory: 256Mi } securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] volumeMounts: - { name: tmp, mountPath: /tmp } volumes: - name: tmp emptyDir: {} ``` **왜 좋은가:** - `agent-pre-populate-only: "true"` → init container만 돌고 sidecar 없음 → 2 pod 당 컨테이너 1개 절감 - etcd에 Kubernetes Secret 생성 없음 (annotation에 명시적 destination 없음; in-memory volume) - template로 `.env` 포맷 렌더링 → legacy 앱이 그대로 소비 --- ## 나쁜 예시 4: Injector + long-lived sidecar + 무한 renew ```yaml annotations: vault.hashicorp.com/agent-inject: "true" vault.hashicorp.com/role: "legacy-app" vault.hashicorp.com/agent-inject-secret-creds: "database/creds/legacy-app" # agent-pre-populate-only 없음 → sidecar 상시 실행 # agent-limits-* 없음 → sidecar가 limit 없이 메모리 증가 ``` **문제:** - sidecar가 Pod 수명 내내 상주 → 1000 서비스 x 3 replica = 3000 추가 컨테이너 - resource limit 미지정 → OOM cascading - VSO로 대체 가능한데 Injector를 default로 쓰면 운영 복잡도 증가 --- ## 좋은 예시 7: Raft snapshot CronJob ```yaml --- apiVersion: v1 kind: ServiceAccount metadata: name: vault-snapshot namespace: vault --- apiVersion: batch/v1 kind: CronJob metadata: name: vault-raft-snapshot namespace: vault spec: schedule: "0 2 * * *" concurrencyPolicy: Forbid successfulJobsHistoryLimit: 3 failedJobsHistoryLimit: 3 jobTemplate: spec: backoffLimit: 2 template: spec: restartPolicy: OnFailure serviceAccountName: vault-snapshot securityContext: runAsNonRoot: true runAsUser: 100 runAsGroup: 1000 fsGroup: 1000 seccompProfile: type: RuntimeDefault containers: - name: snapshot image: hashicorp/vault:1.17.6 env: - name: VAULT_ADDR value: https://vault.vault.svc.cluster.local:8200 - name: VAULT_CACERT value: /vault/tls/ca.crt - name: VAULT_TOKEN valueFrom: secretKeyRef: name: vault-snapshot-token key: token - name: AWS_REGION value: ap-northeast-2 command: - sh - -c - | set -eu TS=$(date -u +%Y%m%dT%H%M%SZ) SNAP=/tmp/vault-${TS}.snap vault operator raft snapshot save "${SNAP}" aws s3 cp "${SNAP}" "s3://vault-backup.example.com/daily/vault-${TS}.snap" \ --sse aws:kms --sse-kms-key-id alias/vault-backup rm -f "${SNAP}" volumeMounts: - name: vault-tls mountPath: /vault/tls readOnly: true - name: tmp mountPath: /tmp resources: requests: cpu: 100m memory: 128Mi limits: cpu: 500m memory: 512Mi securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] volumes: - name: vault-tls secret: secretName: vault-tls - name: tmp emptyDir: {} ``` **왜 좋은가:** - 매일 02:00 UTC `raft snapshot save` 실행 - 결과를 S3 SSE-KMS로 off-cluster 보관 (PVC와 독립적 failure domain) - 짧은 TTL snapshot token을 별도 Secret로 주입 (root token 미사용) - `concurrencyPolicy: Forbid`로 snapshot 중복 방지 --- ## 좋은 예시 8: ServiceMonitor + Prometheus policy ```yaml --- # Vault policy: Prometheus가 /v1/sys/metrics 읽기 전용 # (이 정책은 Vault 내부에 생성) # vault policy write prometheus-metrics - <