HashiCorp Vault
secret을 저장만 하지 않고 요청 시점에 발급한다
Intro
secret은 맨날 털린다.
하드코딩된 API Key, .env File, CI 로그에 찍힌 토큰 등등…
심지어 secret이 여러 곳에 흩어지면 누가 무엇에 접근하는지 아무도 모르고, 유출됐을 때 무엇을 갈아야 하는지도 모른다.
HashiCorp Vault는 이 문제를 하나의 Layer로 모아서 처리한다.
모든 secret을 한 곳에 두고, 그 앞에 인증(authentication), 인가(authorization), 감사(audit)를 세운다.
누가(인증) 무엇에(인가) 접근했는지 전부 기록(감사)하고, 저장된 데이터는 암호화 경계 뒤에 숨긴다.
여기까지는 통상적인 금고의 역할이고, Vault를 금고 이상으로 만드는 것은 동적 secret(dynamic secrets)이다.
정적 비밀번호를 보관하는 대신, 요청이 올 때마다 수명이 짧은 자격증명을 즉석에서 만들어 주고 만료되면 스스로 폐기한다.
The Barrier and Sealing
Vault의 저장 구조를 이해하는 열쇠는 barrier다.
barrier란 Vault가 데이터를 storage backend(디스크, S3, Raft 등)에 쓰기 전에 통과시키는 암호화 경계를 말한다.
barrier 안쪽은 신뢰 영역이고 바깥쪽(storage backend)은 신뢰하지 않는 영역이다.
그래서 storage backend에 실제로 남는 것은 언제나 암호문뿐이고, 스토리지를 통째로 훔쳐도 평문 secret은 나오지 않는다.
- 실제 데이터는 keyring의 encryption key로 암호화된다.
- 그 encryption key는 root key(마스터 키)로 암호화된다.
- 그 root key는 다시 unseal key로 암호화된다.
저장되는 것은 언제나 암호문이다.
Vault 프로세스는 시작할 때 sealed(봉인) 상태다. 이때 Vault는 암호문이 어디 있는지는 알지만 root key를 몰라 아무것도 복호화하지 못한다.
unseal(봉인 해제)이란 unseal key를 제공해 root key를 평문으로 복원하는 과정을 말한다. 봉인이 풀려야 비로소 요청을 처리한다.
문제는 이 unseal key를 한 사람이 통째로 쥐면 그 사람이 곧 단일 실패점이자 단일 공격 대상이 된다는 것이다.
Vault의 기본 답은 Shamir’s Secret Sharing이다.
근데 보통은 auto unseal을 쓴다.
auto unseal은 root key의 복호화를 클라우드 KMS나 HSM 같은 외부 신뢰 장치에 위임해, Vault가 재시작할 때 사람 개입 없이 자동으로 봉인을 푼다. 오토스케일링이나 잦은 재시작이 있는 환경에서 사실상 필수다.
Auth, Policy, Secrets Engines
Unseal된 Vault에 요청이 들어오면 네 단계를 밟는다. 인증, 토큰 발급, secret 작업, 그리고 그 전부를 관통하는 감사다.
먼저 인증(authentication). 클라이언트는 자신이 누구인지 auth method로 증명한다. auth method란 신원 증명 방식을 꽂아 쓰는 Plugin으로, 토큰, AppRole, LDAP, 그리고 쿠버네티스 서비스 어카운트, 클라우드 IAM 등이 있다. 증명에 성공하면 Vault는 그 신원에 정책을 묶은 토큰을 발급한다.
여기서 인가(authorization) 모델은 default-deny다. 정책(policy)이 경로에 대해 명시적으로 허용하지 않는 한 모든 접근은 거부된다. 정책은 “이 경로에 read/create를 허용한다” 식으로 경로 기반으로 쓰고, 토큰에 붙은 정책들의 합집합이 그 토큰의 권한이 된다.
인가를 통과하면 secrets engine에 대고 실제 작업을 한다. secrets engine이란 특정 경로에 마운트돼 secret을 저장하거나 생성하는 컴포넌트로, 두 부류로 갈린다.
- 정적 secret:
kv(key-value) 엔진처럼 우리가 넣어둔 값을 그대로 돌려준다. 기존 비밀번호 금고에 가깝다. - 동적 secret:
database,aws,pki엔진처럼 요청이 올 때마다 자격증명을 새로 만든다. 예컨대database엔진은 앱이 요청하면 PostgreSQL에 수명 1시간짜리 계정을 만들어 건네고, 1시간 뒤 그 계정을 DB에서 삭제한다.
동적 secret과 서비스 토큰에는 모두 lease가 붙는다. lease란 secret의 임대 계약으로, TTL(time-to-live)이 지나면 Vault가 그 자격증명을 자동으로 회수(revoke)한다. 토큰을 revoke하면 그 토큰으로 만든 모든 lease가 함께 회수된다. 유출된 자격증명의 유효 기간이 짧을수록 피해 반경이 줄어든다는 것이 이 설계의 핵심이다.
마지막으로 감사(audit). 인증 성공 여부, 인가 통과 여부와 무관하게 모든 요청과 응답이 audit device에 기록된다. 누가 언제 무엇을 요청했고 결과가 무엇이었는지가 남으므로, 사고가 났을 때 추적이 된다.
Storage Backends
barrier 바깥에서 암호문을 실제로 보관하는 곳이 storage backend다. 선택지는 크게 넷이다.
- Integrated Storage (Raft): Vault 자신이 Raft 합의 알고리즘으로 노드끼리 데이터를 복제한다. 외부 의존성이 없고 고가용성(HA)을 지원해, HashiCorp가 대부분의 배포에 권장하는 기본값이다.
- File system: 단일 노드 로컬 디스크. HA 불가. 개발이나 단일 인스턴스용이다.
- External: Consul, 클라우드 스토리지, RDBMS 등 외부 시스템에 저장. HA 지원 여부는 백엔드마다 다르다.
- In-memory: 프로세스 메모리에만 두고 재시작하면 사라진다. 개발/테스트 전용이다.
HA가 필요하면 답은 Integrated Storage(Raft)다. 과거에는 HA를 위해 Consul을 별도로 운영해야 했지만, Raft가 그 외부 의존성을 없앴다. Raft 클러스터는 리더 하나와 팔로워 여럿으로 구성되고, 리더가 죽으면 남은 노드 중 하나가 선출돼 승계한다. 3노드라면 최소 2노드(과반)가 살아 있어야 정족수(quorum)를 유지한다. 노드 간 통신은 API 포트 8200과 별개로 클러스터 포트 8201을 쓴다.
Vault in Kubernetes
쿠버네티스에서는 앞의 “Vault Agent”가 훨씬 매끄럽게 녹아든다. 그 출발점은 파드가 Vault에 어떻게 자신을 증명하느냐다.
Kubernetes Auth Method
파드에 Vault 토큰을 미리 심어 두면, 그 토큰 자체가 또 다른 secret이 되어 “secret을 안전하게 나눠주려다 secret을 하나 더 만드는” 순환에 빠진다.
Kubernetes auth method를 써서 해결할 수 있다.
파드에는 이미 쿠버네티스가 자동으로 마운트해 주는 서비스 어카운트 토큰(ServiceAccount token)이 있다.
파드는 이 토큰을 그대로 Vault에 내밀고, Vault는 쿠버네티스의 TokenReview API에 “이 토큰이 진짜 이 서비스 어카운트의 것이 맞냐”고 물어 검증한다.
검증되면 그 서비스 어카운트에 매핑된 정책이 붙은 Vault 토큰을 발급한다.
즉 파드가 원래부터 갖고 있던 신원(서비스 어카운트)을 “첫 번째 secret”(Secret Zero)으로 삼아, 별도의 Vault 자격증명을 심을 필요를 없앤다. 이것이 뒤따르는 세 가지 연동 방식의 공통 토대다.
세 가지 연동 경로
파드에 실제 secret을 전달하는 방법은 셋이고, 전달 매체가 서로 다르다.
- Agent Injector (사이드카): mutating admission webhook이 파드 스펙을 가로채,
vault.hashicorp.com/agent-inject같은 어노테이션이 붙은 파드에 Vault Agent 사이드카를 자동으로 끼워 넣는다. 사이드카가 secret을 받아/vault/secrets/아래 메모리 볼륨(tmpfs)에 렌더링하면, Vault를 전혀 모르는 앱 컨테이너가 그냥 그 파일을 읽는다. Vault의 모든 auto-auth 방식을 지원하고, 사이드카가 살아 있으므로 secret 갱신과 회전을 자동으로 해준다. 대신 파드마다 에이전트가 하나씩 붙는 오버헤드가 있다. - CSI Provider (볼륨 마운트): Secrets Store CSI Driver를 통해
SecretProviderClass오브젝트가 정의한 secret을 볼륨으로 마운트한다. secret은 컨테이너 생성(ContainerCreation) 단계에 채워지고, 그때까지 파드는 시작이 막힌다. 노드마다 CSI 드라이버 파드가 하나씩 도는 구조라 파드별 사이드카가 없어 가볍고, Istio 같은 사이드카 충돌에서 자유롭다. 단, Vault Agent를 쓰지 않으므로 자동 회전을 지원하지 않고, auth method도 쿠버네티스 방식만 된다. - External Secrets Operator (ESO):
SecretStore/ExternalSecretCRD로 Vault의 값을 가져와 네이티브 쿠버네티스Secret오브젝트로 실체화한다. 앱은 평범한Secret을 마운트하므로 기존 매니페스트를 거의 안 바꿔도 된다. 대신 secret이 결국 etcd에 일반Secret으로 내려앉는다는 점(Vault 밖으로 복제된다)과, Vault provider가 KV 엔진만 지원한다는 제약을 감안해야 한다. PKI 같은 동적 엔진은 이 경로로 못 가져온다.
셋의 선택은 이렇게 갈린다. secret을 Vault 밖으로 내보내지 않고 자동 Rotate까지 원하면 Agent Injector, 사이드카 없이 가볍게 볼륨으로만 받고 싶으면 CSI Provider, 기존 Secret 기반 워크로드를 최소 변경으로 붙이고 싶으면 ESO다.
| 경로 | 전달 매체 | 자동 회전 | auth method | secret이 etcd에? |
|---|---|---|---|---|
| Agent Injector | 메모리 볼륨 /vault/secrets | O | 모든 auto-auth | 아니오 |
| CSI Provider | CSI 볼륨 마운트 | X | 쿠버네티스만 | 아니오 |
| ESO | 네이티브 Secret | O(재동기화) | 다양 | 예 |
PKI Secrets Engine
pki 엔진은 동적 secret 개념을 인증서로 확장한다. PKI secrets engine이란 요청 시점에 X.509 인증서를 즉석에서 발급하는 secrets engine으로, 개인키 생성 → CSR 작성 → CA 제출 → 검증 → 서명이라는 전통적 수작업을 한 번의 API 호출로 대체한다. 서비스 간 mTLS나 내부 TLS 인증서를 사람 손 없이 뽑아낼 수 있다.
핵심 운영 원칙은 root CA를 Vault에 두지 않는 것이다. 권장 구성은 root CA를 오프라인(에어갭)에 두고, Vault에는 root가 서명한 intermediate CA만 올려 실제 leaf 인증서 발급을 맡기는 것이다. 이렇게 하면 Vault가 뚫려도 최상위 신뢰 앵커는 안전하다. 개념을 보이기 위해 아래 예는 root를 Vault 안에서 생성하지만, 실제로는 root를 밖에서 만들고 CSR을 서명해 들여오는 흐름을 쓴다.
# 1. PKI 엔진 활성화, 최대 lease TTL 10년으로 튜닝
vault secrets enable pki
vault secrets tune -max-lease-ttl=87600h pki
# 2. root CA 생성 (개념 시연용. 실전은 오프라인 root 권장)
vault write pki/root/generate/internal \
common_name="example.com" ttl=87600h
# 3. 발급 URL과 CRL 배포 지점 설정
vault write pki/config/urls \
issuing_certificates="http://vault.vault:8200/v1/pki/ca" \
crl_distribution_points="http://vault.vault:8200/v1/pki/crl"
# 4. 역할(role) 정의 — 이 도메인만, 이 최대 수명으로
vault write pki/roles/example-dot-com \
allowed_domains="example.com" \
allow_subdomains=true max_ttl="72h"
# 5. 인증서 발급
vault write pki/issue/example-dot-com common_name="api.example.com"
role이 정책의 역할을 한다. allowed_domains로 발급 가능한 도메인을 못 박고 max_ttl로 수명을 제한한다. 여기서 TTL을 짧게 유지하는 것이 PKI 운영의 핵심 철학이다. 인증서 수명이 짧으면 폐기(revocation) 자체가 거의 필요 없어지고, CRL(인증서 폐기 목록)이 짧게 유지되며, 인스턴스마다 메모리에만 사는 일회성 인증서를 쓸 수 있다. 긴 수명 인증서를 발급하고 유출 시 폐기에 매달리는 대신, 애초에 금방 만료되게 만드는 쪽이다.
cert-manager 연동
쿠버네티스라면 인증서 발급을 cert-manager에 맡기고 Vault를 그 뒤의 CA로 세우는 조합이 자연스럽다. cert-manager의 Vault Issuer는 Vault의 PKI 엔진을 발급 백엔드로 삼아, Certificate 리소스가 생기면 Vault로 서명 요청을 보내 X.509 인증서를 자동 발급하고 갱신한다.
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: vault-issuer
namespace: default
spec:
vault:
server: http://vault.vault:8200
path: pki/sign/example-dot-com # 반드시 sign 엔드포인트
auth:
kubernetes:
role: issuer # Vault 쪽 role
mountPath: /v1/auth/kubernetes
secretRef:
name: issuer-token
key: token
두 가지 디테일이 자주 발목을 잡는다. 하나, spec.vault.path는 반드시 sign 엔드포인트(pki/sign/...)를 가리켜야 한다. 둘, 쿠버네티스 1.24부터는 서비스 어카운트 토큰 Secret이 자동 생성되지 않으므로, Issuer가 참조할 kubernetes.io/service-account-token 타입 Secret을 명시적으로 만들어 줘야 한다. Vault 서버가 클러스터 안에 있으면 위처럼 Kubernetes auth를, 클러스터 밖에 있으면 JWT/OIDC auth를 쓰는 것이 권장된다.
이렇게 붙이면 애플리케이션은 Certificate 하나만 선언하면 되고, 실제 인증서의 발급과 만료 전 갱신은 cert-manager와 Vault가 알아서 처리한다.
References
- What is Vault? — HashiCorp Developer
https://developer.hashicorp.com/vault/docs/what-is-vault - Vault Internals: Architecture — HashiCorp Developer
https://developer.hashicorp.com/vault/docs/internals/architecture - Vault Concepts (seal/unseal, lease, tokens) — HashiCorp Developer
https://developer.hashicorp.com/vault/docs/concepts - Highly available Vault cluster with integrated storage (Raft) — HashiCorp Developer
https://developer.hashicorp.com/vault/docs/deploy/kubernetes/helm/examples/ha-with-raft - Vault on Kubernetes deployment guide — HashiCorp Developer
https://developer.hashicorp.com/vault/tutorials/kubernetes/kubernetes-raft-deployment-guide - What is Vault Agent? — HashiCorp Developer
https://developer.hashicorp.com/vault/docs/agent-and-proxy/agent - Use Vault Agent templates — HashiCorp Developer
https://developer.hashicorp.com/vault/docs/agent-and-proxy/agent/template - Vault Agent Injector vs. Vault CSI Provider — HashiCorp Developer
https://developer.hashicorp.com/vault/docs/deploy/kubernetes/injector-csi - HashiCorp Vault Provider — External Secrets Operator
https://external-secrets.io/latest/provider/hashicorp-vault/ - HTTP API: Libraries — HashiCorp Developer
https://developer.hashicorp.com/vault/api-docs/libraries - Spring Cloud Vault — Reference
https://docs.spring.io/spring-cloud-vault/reference/ - PKI Secrets Engine — HashiCorp Developer
https://developer.hashicorp.com/vault/docs/secrets/pki - Build your own certificate authority (CA) — HashiCorp Developer
https://developer.hashicorp.com/vault/tutorials/pki/pki-engine - Vault Issuer — cert-manager Documentation
https://cert-manager.io/docs/configuration/vault/ - OpenBao — PKI secrets engine
https://openbao.org/docs/secrets/pki/