Vault 란
Vault는 애플리케이션에서 사용하는 민감한 정보(Secret)를 안전하게 저장하고 필요할 때만 조회할 수 있도록 제공하는 서버이다.
Vault에 대한 모든 접근은 Token 기반으로 제어되며, Token에는 Policy(권한) 가 연결되어 접근 가능한 Secret이 제한된다.
Vault는 보안을 위해 처음 기동 시 Sealed(잠김) 상태로 시작하며, 운영자가 Init → Unseal 과정을 거쳐야만 정상적으로 Secret을 저장·조회할 수 있다.
Kubernetes 환경에서는 Kubernetes Auth를 통해 Pod가 자신에게 할당된 ServiceAccount 토큰(JWT) 으로 Vault에 인증하고, 발급받은 Vault Token을 사용해 Secret을 조회할 수 있다.
장점
- Secret을 중앙에서 안전하게 관리할 수 있다.
- Token + Policy 기반으로 접근 제어가 명확하다.
- Secret 변경 시 애플리케이션 재배포가 필요 없다.
단점
- Init/Unseal, Policy, Auth 설정 등 초기 학습 비용
- 운영 환경에서는 HA 구성과 키 관리에 대한 부담
Vault 실습환경
이전과 동일하게 Windows 11의 WSL2를 통해서 Ubuntu 24.04를 실행하고 Kind를 통해서 구성하였다.
- Host : Windows 11
- WSL2 : Ubuntu 24.04
- docker / kind / kubectl / helm 설치
- vault CLI (로컬에서 Vault API 호출용)
kind 클러스터 생성
kind create cluster --name myk8s --image kindest/node:v1.32.8 --config - <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30000
hostPort: 30000
protocol: TCP
- containerPort: 30001
hostPort: 30001
protocol: TCP
EOF
docker exec -it myk8s-control-plane sh -c 'apt update && apt install -y tree net-tools dnsutils tcpdump ngrep curl vim'
Helm으로 Vault 설치
Helm repo 추가
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
values.yaml 작성 및 설치
NodePort 30000으로 UI/HTTP 접근
cat <<'EOF' > vault-values.yaml
global:
enabled: true
tlsDisable: true
server:
# 여기서는 dev 모드가 아닌 standalone을 사용해, Init/Unseal을 직접 해보는 형태로 진행함
standalone:
enabled: true
config: |
ui = true
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_disable = 1
}
storage "file" {
path = "/vault/data"
}
dataStorage:
enabled: true
size: "10Gi"
service:
enabled: true
type: NodePort
nodePort: 30000
ui:
enabled: true
injector:
enabled: false
EOF
설치
kubectl create ns vault 2>/dev/null || true
helm upgrade --install vault hashicorp/vault -n vault -f vault-values.yaml --version 0.31.0
설치 상태확인
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# kubectl get sts,pods,svc -n vault
NAME READY AGE
statefulset.apps/vault 0/1 77s
NAME READY STATUS RESTARTS AGE
pod/vault-0 0/1 Running 0 77s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/vault NodePort 10.96.242.115 <none> 8200:30000/TCP,8201:30894/TCP 77s
service/vault-internal ClusterIP None <none> 8200/TCP,8201/TCP 77s
service/vault-ui ClusterIP 10.96.235.21 <none> 8200/TCP 77s
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# kubectl exec -ti vault-0 -n vault -- vault status
Key Value
--- -----
Seal Type shamir
Initialized false # 초기화 되어있지 않은것을 확인
Sealed true # 처음은 잠겨있는 상태
Total Shares 0
Threshold 0
Unseal Progress 0/0
Unseal Nonce n/a
Version 1.20.4
Build Date 2025-09-23T13:22:38Z
Storage Type file
HA Enabled false
command terminated with exit code 2
Vault Init/Unseal
Init (Unseal Key & Root Token 생성)
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# cat cluster-keys.json | jq
{
"unseal_keys_b64": [
"****"
],
"unseal_keys_hex": [
"****"
],
"unseal_shares": 1,
"unseal_threshold": 1,
"recovery_keys_b64": [],
"recovery_keys_hex": [],
"recovery_keys_shares": 0,
"recovery_keys_threshold": 0,
"root_token": "hvs.****"
}
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# jq -r ".unseal_keys_b64[]" cluster-keys.json
****
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# jq -r ".root_token" cluster-keys.json
hvs.****
Vault를 초기화(vault operator init)하면 Unseal Key와 Root Token이 생성된다.
이 두 값은 Vault 보안의 핵심이므로, 외부 노출은 피해야 한다.

Unseal Key란?
- 설치 과정에서 확인한 것처럼 Vault는 시작 시 기본적으로 sealed(잠김) 상태로 올라온다.
- Unseal Key는 이 Vault를 unseal(잠금 해제) 하는 데 사용된다.
- Unseal Key가 유출되면 누구나 Vault를 unseal할 수 있음(사실상 Vault의 마스터 키에 해당)
Root Token이란?
- Vault의 최고 권한 토큰이다.
- 모든 정책, 시크릿 엔진, 인증 방식, 사용자 관리 가능
- 초기 설정 및 긴급 상황에서만 사용하는 것이 권장된다고 한다.
- 모든 시크릿 읽기/쓰기/삭제 가능
Unseal (잠금 해제)
export VAULT_UNSEAL_KEY="$(jq -r ".unseal_keys_b64[]" cluster-keys.json)"
kubectl exec vault-0 -n vault -- vault operator unseal "$VAULT_UNSEAL_KEY"
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# kubectl exec -ti vault-0 -n vault -- vault status
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.20.4
Build Date 2025-09-23T13:22:38Z
Storage Type file
Cluster Name vault-cluster-d83b51a1
Cluster ID 7ce81c00-7319-5a84-dffe-cb9b7d19f4d9
HA Enabled false
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# kubectl get pod -n vault
NAME READY STATUS RESTARTS AGE
vault-0 1/1 Running 0 117m
sealed 상태일 땐 readiness 실패였지만, vault-0가 1/1 Running이 되면 Readiness가 통과해 정상 기동한 것을 확인할 수 있다.
로컬 Vault CLI 로그인 (NodePort 30000)
토큰은 cluster-keys.json의 root_token을 입력한다.
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vault
export VAULT_ADDR='http://localhost:30000'
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# vault status
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.20.4
Build Date 2025-09-23T13:22:38Z
Storage Type file
Cluster Name vault-cluster-d83b51a1
Cluster ID 7ce81c00-7319-5a84-dffe-cb9b7d19f4d9
HA Enabled false
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# vault login
Token (will be hidden):
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token hvs.****
token_accessor ****
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
Vault 웹UI 확인
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# kubectl get svc -n vault vault
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
vault NodePort 10.96.242.115 <none> 8200:30000/TCP,8201:30894/TCP 124m


Vault 테스트
KV v2는 API 경로가 /v1/secret/data/... 형태이다.
CLI에서는 secret/webapp/config로 보이지만, 실제 API는 secret/data/webapp/config를 사용하게 된다.
Secret 저장/조회(KV v2)
kv-v2 엔진 활성화 & secret 생성
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# vault secrets enable -path=secret kv-v2
Success! Enabled the kv-v2 secrets engine at: secret/
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# vault kv put secret/webapp/config username="static-user" password="static-password"
====== Secret Path ======
secret/data/webapp/config
======= Metadata =======
Key Value
--- -----
created_time 2025-12-13T16:11:15.683727823Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# vault kv get secret/webapp/config
====== Secret Path ======
secret/data/webapp/config
======= Metadata =======
Key Value
--- -----
created_time 2025-12-13T16:11:15.683727823Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
====== Data ======
Key Value
--- -----
password static-password
username static-user
curl로 API 조회 (X-Vault-Token 헤더)
export VAULT_ROOT_TOKEN="$(jq -r ".root_token" cluster-keys.json)"
curl -s --header "X-Vault-Token: $VAULT_ROOT_TOKEN" \
http://127.0.0.1:30000/v1/secret/data/webapp/config | jq
{
"request_id": "62d5d2f5-e056-4bd5-4268-d8e4d99815d5",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": {
"data": {
"password": "static-password",
"username": "static-user"
},
"metadata": {
"created_time": "2025-12-13T16:11:15.683727823Z",
"custom_metadata": null,
"deletion_time": "",
"destroyed": false,
"version": 1
}
},
"wrap_info": null,
"warnings": null,
"auth": null,
"mount_type": "kv"
}
Kubernetes Auth 구성
아래 흐름으로 동작하는 WebApp을 구성해본다.
JWT(쿠버네티스)로 로그인 → Vault 토큰 발급 → Vault 토큰으로 Secret 읽기 → WebApp 출력
- Vault의 Kubernetes Auth는 Pod의 ServiceAccount 토큰(JWT)을 인증 재료로 사용한다.
- WebApp은 Pod 내부에 마운트된 ServiceAccount JWT를 읽어 Vault의 auth/kubernetes/login 엔드포인트에 전달한다.
- Vault는 Kubernetes API의 TokenReview 등을 통해 JWT 유효성을 검증하고, 성공 시 해당 Role/Policy가 연결된 Vault 토큰을 발급한다.
- WebApp은 발급받은 Vault 토큰으로 Vault KV(Secret Engine)에서 Secret을 읽어와 결과를 출력한다.
auth enable & config
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# vault write auth/kubernetes/config kubernetes_host="https://kubernetes.default.svc"
Success! Data written to: auth/kubernetes/config
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# vault read auth/kubernetes/config
Key Value
--- -----
disable_iss_validation true
disable_local_ca_jwt false
issuer n/a
kubernetes_ca_cert n/a
kubernetes_host https://kubernetes.default.svc
pem_keys []
token_reviewer_jwt_set false
use_annotations_as_alias_metadata false
Policy 만들기
secret/data/webapp/config read 권한 생성
vault policy write webapp - <<'EOF'
path "secret/data/webapp/config" {
capabilities = ["read"]
}
EOF
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# vault policy read webapp
path "secret/data/webapp/config" {
capabilities = ["read"]
}
Role 만들기
ServiceAccount와 Policy를 연결
vault write auth/kubernetes/role/webapp \
bound_service_account_names=vault \
bound_service_account_namespaces=default \
policies=webapp \
ttl=24h \
audience="https://kubernetes.default.svc.cluster.local"
WebApp 배포
실제로 Pod가 Vault에서 Secret을 읽을 수 있는지 아래 과정을 통해 확인한다.
- WebApp이 ServiceAccount JWT를 파일에서 읽고
- Vault의 auth/kubernetes/login으로 JWT+Role을 POST하고
- 받은 Vault Token으로 secret/data/webapp/config를 GET해서 결과를 리턴해준다.
ServiceAccount 생성
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# kubectl create sa vault -n default
serviceaccount/vault created
Deployment + Service(NodePort 30001) 적용
cat <<'EOF' | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
name: webapp
labels:
app: webapp
spec:
replicas: 1
selector:
matchLabels:
app: webapp
template:
metadata:
labels:
app: webapp
spec:
serviceAccountName: vault
containers:
- name: app
image: hashieducation/simple-vault-client:latest
imagePullPolicy: Always
env:
- name: VAULT_ADDR
value: "http://vault.vault.svc:8200"
- name: JWT_PATH
value: "/var/run/secrets/kubernetes.io/serviceaccount/token"
- name: SERVICE_PORT
value: "8080"
volumeMounts:
- name: sa-token
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
volumes:
- name: sa-token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 600
---
apiVersion: v1
kind: Service
metadata:
name: webapp
spec:
selector:
app: webapp
type: NodePort
ports:
- port: 80
targetPort: 8080
protocol: TCP
nodePort: 30001
EOF
배포확인
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# kubectl get pod -l app=webapp
NAME READY STATUS RESTARTS AGE
webapp-9484c6fd7-9s4k9 1/1 Running 0 82s
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# kubectl get svc webapp
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
webapp NodePort 10.96.161.4 <none> 80:30001/TCP 84s
테스트 환경에 따라 약간 Pod 생성에 시간이 걸리는데, 에러가 발생할 경우 아래 로그로 확인
kubectl logs -l app=webapp --tail=200 -f
WebApp 호출로 Secret 확인
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# curl -s http://127.0.0.1:30001
echo
password:static-password username:static-user
웹UI에서 아래와 같이 확인할 수도 있다.

JWT 디코드 후 audience/subject 확인
# padding을 맞춰주려고 조금 귀찮게 명령어가 길어짐
kubectl exec deploy/webapp -- sh -c '
p=$(cut -d "." -f2 /var/run/secrets/kubernetes.io/serviceaccount/token | tr "_-" "/+")
pad=$(( (4 - (${#p} % 4)) % 4 ))
printf "%s%*s" "$p" "$pad" "" | tr " " "=" | base64 -d
' | jq .
{
"aud": [
"https://kubernetes.default.svc.cluster.local"
],
"exp": 1765645091,
"iat": 1765644491,
"iss": "https://kubernetes.default.svc.cluster.local",
"jti": "9bf6b84b-8e4e-4c65-a024-4a84ebf711b3",
"kubernetes.io": {
"namespace": "default",
"node": {
"name": "myk8s-control-plane",
"uid": "1e86398b-1928-44fa-a34b-21016a613dee"
},
"pod": {
"name": "webapp-9484c6fd7-9s4k9",
"uid": "5fa7ac84-5676-4eeb-9cd6-6cbe8a31eeac"
},
"serviceaccount": {
"name": "vault",
"uid": "ddc937a6-dbba-40b8-bf85-a498c493b13c"
}
},
"nbf": 1765644491,
"sub": "system:serviceaccount:default:vault"
}
Vault의 Secret을 변경 후 WebApp 동작 테스트
Vault의 장점 중 하나로 애플리케이션 배포 없이 Secret만 교체하는게 가능하다.
# vault에 입력한 정보 변경
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# vault kv put secret/webapp/config username="changed-user" password="changed-password"
====== Secret Path ======
secret/data/webapp/config
======= Metadata =======
Key Value
--- -----
created_time 2025-12-13T16:54:55.916252198Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 2
# 변경 후 별다른 App 재배포 없이 Secret이 변경된 것을 확인
(⎈|kind-myk8s:N/A) root@DESKTOP-O4EPQ9T:~# curl -s http://127.0.0.1:30001;echo
password:changed-password username:changed-user