이번 포스트에서는 Kubernetes 환경에서 Secret 관리를 위해서 Vault를 활용하는 방식을 살펴 보겠습니다.
실습에서는 Vault에 Secret을 저장하고, 애플리케이션에서 Vault의 Secret을 참조하는 방식을 살펴봅니다. Vault의 Secret을 활용하는 방식에는 세가지가 있습니다.
- The Vault Sidecar Agent Injector
- The Vault Container Storage Interface provider
- The Vault Secrets Operator
이를 실습을 통해서 살펴보겠습니다.
목차
- 실습 환경 구성
- Valut Sidecar Agent Injector 실습
- Vault CSI Driver 실습
- Vault Secrets Operator 실습
1. 실습 환경 구성
실습 환경은 kind로 생성한 쿠버네티스 환경을 통해 진행합니다.
# 클러스터 배포 전 확인
docker ps
mkdir cicd-labs
cd cicd-labs
# WSL2 Ubuntu eth0 IP를 지정
ip -br -c a
MyIP=<각자 자신의 WSL2 Ubuntu eth0 IP>
MyIP=172.28.157.42
# cicd-labs 디렉터리에서 아래 파일 작성
cat > kind-3node.yaml <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
apiServerAddress: "$MyIP"
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30000
hostPort: 30000
- containerPort: 30001
hostPort: 30001
- containerPort: 30002
hostPort: 30002
- containerPort: 30003
hostPort: 30003
- containerPort: 30004
hostPort: 30004
- containerPort: 30005
hostPort: 30006
- role: worker
- role: worker
EOF
kind create cluster --config kind-3node.yaml --name myk8s --image kindest/node:v1.32.2
# 확인
kind get nodes --name myk8s
myk8s-worker2
myk8s-control-plane
myk8s-worker
# kind 는 별도 도커 네트워크 생성 후 사용 : 기본값 172.18.0.0/16
docker network ls
NETWORK ID NAME DRIVER SCOPE
9b31821d4e20 bridge bridge local
31ba753a11a7 host host local
d91da96d2114 kind bridge local
f2e13485b121 none null local
docker inspect kind |jq
[
{
"Name": "kind",
"Id": "d91da96d2114ac123cfadc41d90d4ef2be33b049c1e369b4acaa38249d367a4d",
"Created": "2025-03-27T22:07:47.108546778+09:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": true,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
},
{
"Subnet": "fc00:f853:ccd:e793::/64",
"Gateway": "fc00:f853:ccd:e793::1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"10a9d53e4bd22ce9fc210c6693f8087185686eb51aa9d58696e0d95dafa24b6c": {
"Name": "myk8s-worker2",
"EndpointID": "b3cb0b47353edc3e6aaa258bd6729d94ea648bc7fcba661fa6c1287e7f349923",
"MacAddress": "02:42:ac:12:00:03",
"IPv4Address": "172.18.0.3/16",
"IPv6Address": "fc00:f853:ccd:e793::3/64"
},
"595658837c5ff4934afbd6a2bcf1c23047490e22825e78697ff311a92cef88d9": {
"Name": "myk8s-worker",
"EndpointID": "6d011d2675ca64750be2c1c35a9cdff22f714de2ca72bebee05bc62e11686205",
"MacAddress": "02:42:ac:12:00:04",
"IPv4Address": "172.18.0.4/16",
"IPv6Address": "fc00:f853:ccd:e793::4/64"
},
"ae13bcf2982f31e171f89953a89471524480e994c924349162d31e7795e0c369": {
"Name": "myk8s-control-plane",
"EndpointID": "e35cfd5f5280307273fc4a6c26fd8311b3dcb32d1fb3d9dd0f55f2ad32f0b515",
"MacAddress": "02:42:ac:12:00:02",
"IPv4Address": "172.18.0.2/16",
"IPv6Address": "fc00:f853:ccd:e793::2/64"
}
},
"Options": {
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
# k8s api 주소 확인
kubectl cluster-info
Kubernetes control plane is running at https://172.28.157.42:41243
CoreDNS is running at https://172.28.157.42:41243/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
# 노드 정보 확인 : CRI 는 containerd 사용
kubectl get node -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
myk8s-control-plane Ready control-plane 12m v1.32.2 172.18.0.2 <none> Debian GNU/Linux 12 (bookworm) 5.15.167.4-microsoft-standard-WSL2 containerd://2.0.3
myk8s-worker Ready <none> 11m v1.32.2 172.18.0.4 <none> Debian GNU/Linux 12 (bookworm) 5.15.167.4-microsoft-standard-WSL2 containerd://2.0.3
myk8s-worker2 Ready <none> 11m v1.32.2 172.18.0.3 <none> Debian GNU/Linux 12 (bookworm) 5.15.167.4-microsoft-standard-WSL2 containerd://2.0.3
# 파드 정보 확인 : CNI 는 kindnet 사용
kubectl get pod -A -o wide
NAMESPACE NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
kube-system coredns-668d6bf9bc-cxqxb 1/1 Running 0 12m 10.244.0.4 myk8s-control-plane <none> <none>
kube-system coredns-668d6bf9bc-g45j9 1/1 Running 0 12m 10.244.0.3 myk8s-control-plane <none> <none>
kube-system etcd-myk8s-control-plane 1/1 Running 0 12m 172.18.0.2 myk8s-control-plane <none> <none>
kube-system kindnet-fclmw 1/1 Running 0 12m 172.18.0.3 myk8s-worker2 <none> <none>
kube-system kindnet-gfxg4 1/1 Running 0 12m 172.18.0.2 myk8s-control-plane <none> <none>
kube-system kindnet-vvqvp 1/1 Running 0 12m 172.18.0.4 myk8s-worker <none> <none>
kube-system kube-apiserver-myk8s-control-plane 1/1 Running 0 12m 172.18.0.2 myk8s-control-plane <none> <none>
kube-system kube-controller-manager-myk8s-control-plane 1/1 Running 0 12m 172.18.0.2 myk8s-control-plane <none> <none>
kube-system kube-proxy-7sgcd 1/1 Running 0 12m 172.18.0.4 myk8s-worker <none> <none>
kube-system kube-proxy-8xck8 1/1 Running 0 12m 172.18.0.3 myk8s-worker2 <none> <none>
kube-system kube-proxy-tpvwq 1/1 Running 0 12m 172.18.0.2 myk8s-control-plane <none> <none>
kube-system kube-scheduler-myk8s-control-plane 1/1 Running 0 12m 172.18.0.2 myk8s-control-plane <none> <none>
local-path-storage local-path-provisioner-7dc846544d-p9l6q 1/1 Running 0 12m 10.244.0.2 myk8s-control-plane <none> <none>
# 컨트롤플레인/워커 노드(컨테이너) 확인 : 도커 컨테이너 이름은 myk8s-control-plane , myk8s-worker/worker-2 임을 확인
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
10a9d53e4bd2 kindest/node:v1.32.2 "/usr/local/bin/entr…" 13 minutes ago Up 13 minutes myk8s-worker2
ae13bcf2982f kindest/node:v1.32.2 "/usr/local/bin/entr…" 13 minutes ago Up 13 minutes 0.0.0.0:30000-30004->30000-30004/tcp, 172.28.157.42:41243->6443/tcp, 0.0.0.0:30006->30005/tcp myk8s-control-plane
595658837c5f kindest/node:v1.32.2 "/usr/local/bin/entr…" 13 minutes ago Up 13 minutes myk8s-worker
이제 생성된 쿠버네티스 환경에 Vault를 설치하도록 하겠습니다.
# Create a Kubernetes namespace.
kubectl create namespace vault
# Setup Helm repo
helm repo add hashicorp https://helm.releases.hashicorp.com
# Check that you have access to the chart.
helm search repo hashicorp/vault
NAME CHART VERSION APP VERSION DESCRIPTION
hashicorp/vault 0.30.0 1.19.0 Official HashiCorp Vault Chart
hashicorp/vault-secrets-gateway 0.0.2 0.1.0 A Helm chart for Kubernetes
hashicorp/vault-secrets-operator 0.10.0 0.10.0 Official Vault Secrets Operator Chart
Helm 차트에 사용할 values 파일을 생성합니다.
cat <<EOF > vault-values.yaml
global:
enabled: true
tlsDisable: true # Disable TLS for demo purposes
server:
image:
repository: "hashicorp/vault"
tag: "1.19.0"
standalone:
enabled: true
replicas: 1
config: |
ui = true
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_disable = 1
}
storage "file" {
path = "/vault/data"
}
service:
enabled: true
type: NodePort
port: 8200
targetPort: 8200
nodePort: 30000 # 🔥 Kind에서 열어둔 포트 중 하나 사용
injector:
enabled: true
csi:
enabled: true
EOF
실습에서는 테스트 환경이므로 standalone 방식으로 구성하였습니다.
또한 injector를 enable하여, sidecar injector를 사용할 수 있도록 정의하였고, csi 드라이버 테스트를 위해 csi 또한 enable하였습니다.
이제 Helm으로 Vault를 배포합니다.
# Helm Install 실행
helm upgrade vault hashicorp/vault -n vault -f vault-values.yaml --install
# 배포확인
kubectl get pods,svc,pvc -n vault
NAME READY STATUS RESTARTS AGE
pod/vault-0 0/1 Running 0 4m45s
pod/vault-agent-injector-56459c7545-fnv9t 1/1 Running 0 4m45s
pod/vault-csi-provider-79rg5 2/2 Running 0 4m45s
pod/vault-csi-provider-x58j8 2/2 Running 0 4m45s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/vault NodePort 10.96.116.44 <none> 8200:30000/TCP,8201:31459/TCP 4m45s
service/vault-agent-injector-svc ClusterIP 10.96.166.73 <none> 443/TCP 4m45s
service/vault-internal ClusterIP None <none> 8200/TCP,8201/TCP 4m45s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/data-vault-0 Bound pvc-e5065e5d-dc6e-43ef-a647-da4b0685a79e 10Gi RWO standard <unset> 4m45s
배포된 내용을 확인해보면 vault-0가 statefulset으로 실행 중이며, 옵션으로 추가한 vault-agent-injector-56459c7545-9n94t가 이후에 sidecar injection을 수행합니다. 또한 vault-csi-provider가 실행 중인 것을 알 수 있습니다.
확인해보면 vault-0가 아직 READY가 되지 않은 상태(0/1)인 것을 알 수 있습니다. Vault를 배포한다고 바로 실행되는 것은 아니며, 초기화 되지 않은 상태에서는 기본적으로 Sealed 되어 있습니다.
아래와 같이 상태를 확인하고 초기화를 진행합니다.
# Vault Status 명령으로 Sealed 상태확인
kubectl exec -it 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.19.0
Build Date 2025-03-04T12:36:40Z
Storage Type file
HA Enabled false
command terminated with exit code 2
실습에서는 아래 스크립트를 통해서 unseal key를 획득하고 이 key를 등록하여 초기화를 진행합니다.
cat <<EOF > init-unseal.sh
#!/bin/bash
# Vault Pod 이름
VAULT_POD="vault-0"
# Vault 명령 실행
VAULT_CMD="kubectl exec -it \$VAULT_POD -n vault -- vault"
# 출력 저장 파일
VAULT_KEYS_FILE="./vault-keys.txt"
UNSEAL_KEY_FILE="./vault-unseal-key.txt"
ROOT_TOKEN_FILE="./vault-root-token.txt"
# Vault 초기화 (Unseal Key 1개만 생성되도록 설정)
\$VAULT_CMD operator init -key-shares=1 -key-threshold=1 | sed \$'s/\\x1b\\[[0-9;]*m//g' | tr -d '\r' > "\$VAULT_KEYS_FILE"
# Unseal Key / Root Token 추출
grep 'Unseal Key 1:' "\$VAULT_KEYS_FILE" | awk -F': ' '{print \$2}' > "\$UNSEAL_KEY_FILE"
grep 'Initial Root Token:' "\$VAULT_KEYS_FILE" | awk -F': ' '{print \$2}' > "\$ROOT_TOKEN_FILE"
# Unseal 수행
UNSEAL_KEY=\$(cat "\$UNSEAL_KEY_FILE")
\$VAULT_CMD operator unseal "\$UNSEAL_KEY"
# 결과 출력
echo "[🔓] Vault Unsealed!"
echo "[🔐] Root Token: \$(cat \$ROOT_TOKEN_FILE)"
EOF
# 실행 권한 부여
chmod +x init-unseal.sh
# 실행
./init-unseal.sh
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.19.0
Build Date 2025-03-04T12:36:40Z
Storage Type file
Cluster Name vault-cluster-74a36fbf
Cluster ID 46a5b7ff-288c-eb37-408b-bf8eebd7960c
HA Enabled false
[🔓] Vault Unsealed!
[🔐] Root Token: hvs.aRGCNIlhHVcy2VuDf2afTKCm
이제 vault status
명령으로 Unseal 상태를 확인 합니다.
kubectl exec -it vault-0 -n vault -- vault status
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.19.0
Build Date 2025-03-04T12:36:40Z
Storage Type file
Cluster Name vault-cluster-74a36fbf
Cluster ID 46a5b7ff-288c-eb37-408b-bf8eebd7960c
HA Enabled false
Unseal이 완료되고 파드 상태를 조회하면, 이제 vault-0가 READY상태가 된 것으로 확인됩니다.
kubectl get po -n vault
NAME READY STATUS RESTARTS AGE
vault-0 1/1 Running 0 10m
vault-agent-injector-56459c7545-fnv9t 1/1 Running 0 10m
vault-csi-provider-79rg5 2/2 Running 0 10m
vault-csi-provider-x58j8 2/2 Running 0 10m
초기화가 마무리되면 이제 UI에도 접속이 가능합니다. 앞서 확인한 Root token을 사용합니다.
로그인 후 Vault의 UI화면은 아래와 같습니다.
아래와 같이 WSL 환경에 CLI도 설치를 진행합니다.
참고: https://developer.hashicorp.com/vault/install#linux
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 $(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'
vault status
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.19.0
Build Date 2025-03-04T12:36:40Z
Storage Type file
Cluster Name vault-cluster-74a36fbf
Cluster ID 46a5b7ff-288c-eb37-408b-bf8eebd7960c
HA Enabled false
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.aRGCNIlhHVcy2VuDf2afTKCm
token_accessor LckN5gmpyUcQPxKYFGWIS3KU
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
Vault를 Static Secret, Dynamic Secret, Data Encryption, Leasing and Renewal, Revocation과 같은 다양한 목적에 맞게 사용할 수 있습니다.
이후 Static Secret으로 활용하는 실습을 위해서 Vault의 Key Value 엔진을 활성화하겠습니다.
실습에서는 Vault KV version 2 엔진을 활성화하고 샘플 데이터를 저장합니다. 여기서 version 1 은 Key-Value에 대한 버전관리가 불가하며, version 2에서는 Key-Value에서 버전관리가 가능합니다.
# KV v2 형태로 엔진 활성화
vault secrets enable -path=secret kv-v2
Success! Enabled the kv-v2 secrets engine at: secret/
# 샘플 시크릿 저장
vault kv put secret/sampleapp/config \
username="demo" \
password="p@ssw0rd"
======== Secret Path ========
secret/data/sampleapp/config
======= Metadata =======
Key Value
--- -----
created_time 2025-04-12T13:18:05.548287122Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
# 입력된 데이터 확인
vault kv get secret/sampleapp/config
======== Secret Path ========
secret/data/sampleapp/config
======= Metadata =======
Key Value
--- -----
created_time 2025-04-12T13:18:05.548287122Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
====== Data ======
Key Value
--- -----
password p@ssw0rd
username demo
Vault UI에 접속하여 [Secrets Engine] 탭에 접속 후 secret으로 이동하여, sampleapp - config] 접속하여 정보를 확인합니다.
Secret 탭에 접근하면 실제 저장된 Key / Value 를 확인할 수 있습니다.
Vault에 저장된 Secret을 활용하는 방식을 실습을 통해 살펴보겠습니다.
1. Valut Sidecar Agent Injector 실습
생성된 Vault의 Secret을 참조하기 위해서 Sidecar 패턴을 활용하는 방식을 살펴보겠습니다.
앞서 Helm을 통해 Vault를 배포할 때 injector
를 활성화 한 것을 기억하실 겁니다.
Vault Agent Injector는 Kubernetes Pod 내부에 Vault Agent를 자동으로 주입해주는 기능입니다. 이를 통해 어플리케이션이 Vault로부터 자동으로 비밀 정보를 받아올 수 있게 됩니다.
먼저 그림의 좌측을 살펴보면 애플리케이션 파드 스펙에 Sidecar Annotation과 함께 API 서버로의 요청하면, Mutating Webhook을 통해 SideCar Injector Service를 통해서 Pods Spec을 업데이트 하는 것을 알 수 있습니다.
우측의 애플리케이션 파드에서는 Vault Agent Init Container를 통해서 Kubernetes Auth를 통해서 Vault Token을 획득하고, 이후 Vault Agent Sicdcar Container가 Vault Token으로 Vault에 요청해 Secret을 획득해 애플리케이션에 주입해주는 방식으로 동작합니다.
실제 파드에 아래와 같은 annotation을 추가해야 합니다.
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "example-role"
Vault의 Kubernetes 인증 활성화합니다. 이를 통해 클라이언트가 쿠버네티스의 Service Account 토큰을 통해 인증을 받을 수 있도록 제공합니다.
# Kubernetes Auth Method 활성화
vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/
# 확인
vault auth list
Path Type Accessor Description Version
---- ---- -------- ----------- -------
approle/ approle auth_approle_54604e8f n/a n/a
kubernetes/ kubernetes auth_kubernetes_986afb3c n/a n/a
token/ token auth_token_ecdae7a6 token based credentials n/a
# Kubernetes Auth Config 설정
vault write auth/kubernetes/config \
token_reviewer_jwt="$(kubectl get secret $(kubectl get serviceaccount vault -n vault -o jsonpath='{.secrets[0].name}') -n vault -o jsonpath="{.data.token}" | base64 --decode)" \
kubernetes_host="$(kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[0].cluster.server}')" \
kubernetes_ca_cert="$(kubectl get secret $(kubectl get serviceaccount vault -n vault -o jsonpath='{.secrets[0].name}') -n vault -o jsonpath='{.data.ca\.crt}' | base64 --decode)"
그리고 생성된 Secret을 가져올 수 있는 권한(Policy)와 쿠버네티스 Service Account와 Policy를 묶어주는 Role을 생성합니다.
# 필요한 Policy 작성 (앞선 과정에서 만들었으므로 생략가능)
vault policy write sampleapp-policy - <<EOF
path "secret/data/sampleapp/*" {
capabilities = ["read"]
}
EOF
# Role 생성 (Injector가 로그인할 수 있도록)
vault write auth/kubernetes/role/sampleapp-role \
bound_service_account_names="vault-ui-sa" \
bound_service_account_namespaces="vault" \
policies="sampleapp-policy" \
ttl="24h"
Vault의 권한 부여 방식은 AWS와 유사합니다. 위의 예제를 살펴보면, "secret/data/sampleapp/*" 에 ["read"]를 할 수 있는 sampleapp-policy라는 Policy를 만들고, 그 이후 sampleapp-role라는 role을 만들어 vault-ui-sa와 sampleapp-policy를 연결해 주는 것을 알 수 있습니다.
테스트 애플리케이션 배포합니다. 또한 위에서 정의한 vault-ui-sa 라는 Service Account도 같이 생성합니다.
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-ui-sa
namespace: vault
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: vault-injected-ui
namespace: vault
spec:
replicas: 1
selector:
matchLabels:
app: vault-injected-ui
template:
metadata:
labels:
app: vault-injected-ui
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "sampleapp-role"
vault.hashicorp.com/agent-inject-secret-config.json: "secret/data/sampleapp/config"
vault.hashicorp.com/agent-inject-template-config.json: |
{{- with secret "secret/data/sampleapp/config" -}}
{
"username": "{{ .Data.data.username }}",
"password": "{{ .Data.data.password }}"
}
{{- end }}
vault.hashicorp.com/agent-inject-output-path: "/vault/secrets"
spec:
serviceAccountName: vault-ui-sa
containers:
- name: app
image: python:3.10
ports:
- containerPort: 5000
command: ["sh", "-c"]
args:
- |
pip install flask && cat <<PYEOF > /app.py
import json, time
from flask import Flask, render_template_string
app = Flask(__name__)
while True:
try:
with open("/vault/secrets/config.json") as f:
secret = json.load(f)
break
except:
time.sleep(1)
@app.route("/")
def index():
return render_template_string("<h2>🔐 Vault Injected UI</h2><p>👤 사용자: {{username}}</p><p>🔑 비밀번호: {{password}}</p>", secret)
app.run(host="0.0.0.0", port=5000)
PYEOF
python /app.py
---
apiVersion: v1
kind: Service
metadata:
name: vault-injected-ui
namespace: vault
spec:
type: NodePort
ports:
- port: 5000
targetPort: 5000
nodePort: 30002
selector:
app: vault-injected-ui
EOF
Deployment의 spec을 잘 살펴보면, 아래와 같은 annotation을 명시하고 있습니다.
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "sampleapp-role"
생성된 컨테이너 확인를 확인해보겠습니다.
# 파드 내에 사이드카 컨테이너 추가되어 2/2 확인
kubectl get pod -l app=vault-injected-ui -n vault
NAME READY STATUS RESTARTS AGE
vault-injected-ui-77fb865789-f6sn8 2/2 Running 0 2m
kubectl describe pod -l app=vault-injected-ui -n vault
...
Init Containers:
vault-agent-init:
Container ID: containerd://aea5f02cc79d2b89d6f80a5111ae8b64640d0e13d0900fbe06f3eb3750389de1
Image: hashicorp/vault:1.19.0
Image ID: docker.io/hashicorp/vault@sha256:bbb7f98dc67d9ebdda1256de288df1cb9a5450990e48338043690bee3b332c90
Port: <none>
Host Port: <none>
Command:
/bin/sh
-ec
Args:
echo ${VAULT_CONFIG?} | base64 -d > /home/vault/config.json && vault agent -config=/home/vault/config.json
...
Containers:
app:
Container ID: containerd://e2caeef00e2e408a4db48e57cba1152b4496bd49d6b66d9571371c2efea64178
Image: python:3.10
Image ID: docker.io/library/python@sha256:e2c7fb05741c735679b26eda7dd34575151079f8c615875fbefe401972b14d85
Port: 5000/TCP
Host Port: 0/TCP
...
vault-agent:
Container ID: containerd://a2acf0a3fda4ba1f5637d8e843718ec19382c3011d8db97e701bae0d457fb92c
Image: hashicorp/vault:1.19.0
Image ID: docker.io/hashicorp/vault@sha256:bbb7f98dc67d9ebdda1256de288df1cb9a5450990e48338043690bee3b332c90
Port: <none>
Host Port: <none>
Command:
/bin/sh
-ec
...
앞서 살펴본 것과 같이 vault-agent-init 이라는 InitContainer가 있으며, 또한 app 컨테이너 하단을 보면 vault-agent가 사이드카로 실행 중입니다.
# 볼륨 마운트 확인
kubectl exec -it deploy/vault-injected-ui -c app -n vault -- ls /vault/secrets
config.json
kubectl exec -it deploy/vault-injected-ui -c app -n vault -- cat /vault/secrets/config.json
{
"username": "demo",
"password": "p@ssw0rd"
}
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- ls -l /etc/secrets
total 4
-rw-r--r-- 1 vault vault 94 Apr 10 02:09 index.html
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/secrets/index.html
<html>
<body>
<p>username: demo</p>
<p>password: p@ssw0rd</p>
</body>
</html>
kubectl exec -it deploy/nginx-vault-demo -c nginx -- cat /usr/share/nginx/html/index.html
<html>
<body>
<p>username: demo</p>
<p>password: p@ssw0rd</p>
</body>
</html>
# 로그 확인
kubectl logs -l app=vault-injected-ui -c vault-agent -n vault
2025-04-12T13:37:10.607Z [INFO] agent: (runner) creating watcher
2025-04-12T13:37:10.612Z [INFO] agent.auth.handler: authentication successful, sending token to sinks
2025-04-12T13:37:10.612Z [INFO] agent.auth.handler: starting renewal process
2025-04-12T13:37:10.613Z [INFO] agent.sink.file: token written: path=/home/vault/.vault-token
2025-04-12T13:37:10.613Z [INFO] agent.template.server: template server received new token
2025-04-12T13:37:10.614Z [INFO] agent: (runner) stopping
2025-04-12T13:37:10.614Z [INFO] agent: (runner) creating new runner (dry: false, once: false)
2025-04-12T13:37:10.614Z [INFO] agent: (runner) creating watcher
2025-04-12T13:37:10.617Z [INFO] agent: (runner) starting
2025-04-12T13:37:10.618Z [INFO] agent.auth.handler: renewed auth token
# mutating admission
kubectl get mutatingwebhookconfigurations.admissionregistration.k8s.io
NAME WEBHOOKS AGE
vault-agent-injector-cfg 1 82m
kubectl describe mutatingwebhookconfigurations.admissionregistration.k8s.io vault-agent-injector-cfg
Name: vault-agent-injector-cfg
Namespace:
Labels: app.kubernetes.io/instance=vault
app.kubernetes.io/managed-by=Helm
app.kubernetes.io/name=vault-agent-injector
Annotations: meta.helm.sh/release-name: vault
meta.helm.sh/release-namespace: vault
API Version: admissionregistration.k8s.io/v1
Kind: MutatingWebhookConfiguration
Metadata:
Creation Timestamp: 2025-04-12T12:29:06Z
Generation: 2
Resource Version: 2170
UID: 1260c09d-a982-47f6-b15a-b3de37628915
Webhooks:
Admission Review Versions:
v1
v1beta1
Client Config:
Ca Bundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNTVENDQWU2Z0F3SUJBZ0lVZjZGQ0xaam5iTjg1TURSWm5Ka0VWZ1pMcnhrd0NnWUlLb1pJemowRUF3SXcKR2pFWU1CWUdBMVVFQXhNUFFXZGxiblFnU1c1cVpXTjBJRU5CTUI0WERUSTFNRFF4TWpFeU1qZ3pNRm9YRFRNMQpNRFF4TURFeU1qa3pNRm93R2pFWU1CWUdBMVVFQXhNUFFXZGxiblFnU1c1cVpXTjBJRU5CTUZrd0V3WUhLb1pJCnpqMENBUVlJS29aSXpqMERBUWNEUWdBRVhSbjR3Z0lYSUhKTFN6cnV5MGhDZkV0RWtnN0NHYUp6aFo1TkdKMTMKTXlnVkgzL2NISS9pQ0ZrbG5HWFRsQ3ZjZzUySjlXL0tnUVljSjg2NEVVaUxLS09DQVJBd2dnRU1NQTRHQTFVZApEd0VCL3dRRUF3SUNoREFUQmdOVkhTVUVEREFLQmdnckJnRUZCUWNEQVRBUEJnTlZIUk1CQWY4RUJUQURBUUgvCk1HZ0dBMVVkRGdSaEJGOHdOVG8zWlRwak56b3haam93TXpwbE1UcGtaanBpTnpwaU1EcGxOam80WlRwaVpUcGgKWWpvMk9UbzRaam81TnpvMllUbzFOam96T0RveE16bzFOem94WlRvMllUb3lZenBsTkRvNU1qb3pOem93WlRvdwpORG94TXpvNU9EbzJNekJxQmdOVkhTTUVZekJoZ0Y4d05UbzNaVHBqTnpveFpqb3dNenBsTVRwa1pqcGlOenBpCk1EcGxOam80WlRwaVpUcGhZam8yT1RvNFpqbzVOem8yWVRvMU5qb3pPRG94TXpvMU56b3haVG8yWVRveVl6cGwKTkRvNU1qb3pOem93WlRvd05Eb3hNem81T0RvMk16QUtCZ2dxaGtqT1BRUURBZ05KQURCR0FpRUExKzdRaE5HKwpkM1NRajdZRVdHRHptcXpOUVozU2dtR1ExemQwWHI5L2ZRSUNJUURBQTcwODRDOTY4WVdnUXhPSmQvKysxZm9XCndoUWJ0Rm9taUlPNnMvSkZVZz09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
Service:
Name: vault-agent-injector-svc
Namespace: vault
Path: /mutate
Port: 443
Failure Policy: Ignore
Match Policy: Exact
Name: vault.hashicorp.com
Namespace Selector:
Object Selector:
Match Expressions:
Key: app.kubernetes.io/name
Operator: NotIn
Values:
vault-agent-injector
Reinvocation Policy: Never
Rules:
API Groups:
API Versions:
v1
Operations:
CREATE
Resources:
pods
Scope: Namespaced
Side Effects: None
Timeout Seconds: 30
Events: <none>
kubectl get svc -n vault
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
vault NodePort 10.96.116.44 <none> 8200:30000/TCP,8201:31459/TCP 83m
vault-agent-injector-svc ClusterIP 10.96.166.73 <none> 443/TCP 83m
vault-injected-ui NodePort 10.96.2.33 <none> 5000:30002/TCP 16m
vault-internal ClusterIP None <none> 8200/TCP,8201/TCP 83m
mutation webhook configuration 정의를 보면 vault-agent-injector-svc로 연결되는 것을 확인할 수 있습니다.
생성된 서비스로 접속해보면 Vault에 저장된 Secret 값이 노출되는 것을 알 수있습니다.
2. Vault CSI Driver 실습
Vault에서 setcret을 사용하는 두번째 방식은 CSI Driver를 사용하는 방식입니다.
이를 위해서 Vault 생성 시에 옵션을 통해서 Vault CSI provider가 생성되어 있습니다. 이후 Secrets Store CSI Driver를 설치하면 각 노드에서 Daemonset이 생성됩니다. 사용자는 SecretProviderClass를 생성하고, 여기에는 Secret을 가져오기 위해 어떤 Secret Provider를 사용할지를 선언합니다.
그림을 보면 CSI Volume을 요청하는 파드가 생성되면, CSI Secret Store Driver는 Vault CSI provider에게 요청을 보내고 Vault CSI provider가 SecretProviderClass와 파드의 service Account를 사용해 Vault로 부터 secret을 가져와 파드의 CSI volume으로 마운트 합니다.
아래 문서를 바탕으로 실습을 진행했습니다.
먼저 Vault는 이미 설치된 상태이기 때문에 아래 내용은 skip하겠습니다.
# 리포 추가
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
# Vault 설치
helm install vault hashicorp/vault \
--set "server.dev.enabled=true" \
--set "injector.enabled=false" \
--set "csi.enabled=true"
또한 Secret을 생성과 Kubernetes 인증 설정을 진행합니다.
# Secret 생성
# kubectl exec -it vault-0 -- /bin/sh
vault kv put secret/db-pass password="db-secret-password"
=== Secret Path ===
secret/data/db-pass
======= Metadata =======
Key Value
--- -----
created_time 2025-04-12T14:45:47.117669981Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
vault kv get secret/db-pass
=== Secret Path ===
secret/data/db-pass
======= Metadata =======
Key Value
--- -----
created_time 2025-04-12T14:45:47.117669981Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
====== Data ======
Key Value
--- -----
password db-secret-password
# Kubernetes 인증 설정 (앞서 설정하였으므로 skip)
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
# policy 생성
vault policy write internal-app - <<EOF
path "secret/data/db-pass" {
capabilities = ["read"]
}
EOF
# role 생성
vault write auth/kubernetes/role/database \
bound_service_account_names=webapp-sa \
bound_service_account_namespaces=default \
policies=internal-app \
ttl=20m
이제 클러스터에 secrets store CSI driver를 설치합니다.
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi secrets-store-csi-driver/secrets-store-csi-driver \
--set syncSecret.enabled=true -n vault
# 확인
kubectl get ds -n vault
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
csi-secrets-store-csi-driver 3 3 3 3 3 kubernetes.io/os=linux 77s
vault-csi-provider 2 2 2 2 2 <none> 133m
kubectl get po -n vault
NAME READY STATUS RESTARTS AGE
csi-secrets-store-csi-driver-kmkqf 3/3 Running 0 71s
csi-secrets-store-csi-driver-tct7q 3/3 Running 0 71s
csi-secrets-store-csi-driver-xvfjb 3/3 Running 0 71s
vault-0 1/1 Running 0 133m
vault-agent-injector-56459c7545-fnv9t 1/1 Running 0 133m
vault-csi-provider-79rg5 2/2 Running 0 133m
vault-csi-provider-x58j8 2/2 Running 0 133m
vault-injected-ui-77fb865789-f6sn8 2/2 Running 0 66m
Vault를 provider로 지정한 SecretProviderClass를 생성하고, 앞서 생성한 role을 사용합니다.
cat > spc-vault-database.yaml <<EOF
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-database
spec:
provider: vault
parameters:
vaultAddress: "http://vault:8200"
roleName: "database"
objects: |
- objectName: "db-password"
secretPath: "secret/data/db-pass"
secretKey: "password"
EOF
kubectl apply --filename spc-vault-database.yaml
Role에 지정한 Service Account를 생성합니다.
kubectl create serviceaccount webapp-sa
아래와 같이 파드를 생성하면서, Volume절에 앞서 생성한 secretProviderClass를 참조하는 볼륨을 생성합니다.
cat > webapp-pod.yaml <<EOF
kind: Pod
apiVersion: v1
metadata:
name: webapp
spec:
serviceAccountName: webapp-sa
containers:
- image: jweissig/app:0.0.1
name: webapp
volumeMounts:
- name: secrets-store-inline
mountPath: "/mnt/secrets-store"
readOnly: true
volumes:
- name: secrets-store-inline
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "vault-database"
EOF
kubectl apply --filename webapp-pod.yaml
확인해보면 secretProviderClass를 통해 Vault의 Secret을 마운트 한 것을 확인할 수 있습니다.
kubectl get pods
NAME READY STATUS RESTARTS AGE
webapp 1/1 Running 0 19s
kubectl exec webapp -- cat /mnt/secrets-store/db-password
db-secret-password
4. Vault Secrets Operator(VSO) 실습
앞서 살펴본 방식들은 워크로드에서 Sidecar를 설정하거나 혹은 CSI Volume를 추가하는 방식으로 변경이 필요로 하게 됩니다. 이러한 방식은 기존 리소스를 수정해야 하는 단점이 있습니다.
반면 VSO는 기존 쿠버네티스의 Secret을 사용하되, VSO를 통해서 관리하고자 하는 Secret을 지정하면 VSO가 Vault를 통해 Secret을 가져와 쿠버네티스의 Secret을 변경하는 방식을 사용합니다.
이를 위해서 Vault Secrets Operator는 Vault Role에 대응하는 Vault Auth CRD와 Vault Secret에 대응하는 VaultSecret과 같은 CRD를 생성하고, 이들 CRD의 변경 사항을 감시하여 동작합니다. 각 CRD는 Secret 소스에서 가져와 Kubernetes Secret으로 동기화할 수 있도록 필요한 사양을 제공합니다.
오퍼레이터는 원본 Secret 데이터를 대상 쿠버네티스 Secret에 직접 작성하며, 원본에 변경 사항이 발생할 경우 해당 내용을 대상 Secret에도 수명 주기 동안 지속적으로 반영합니다. 이렇게 함으로써 애플리케이션은 쿠버네티스 Secret에만 접근하면 Secret의 데이터를 사용할 수 있습니다.
즉, 위의 그림을 보면 Vault 연결과 관련된 VaultConnection과 VaultAuth와 같은 CRD를 가지며, 또한 Static, Dynamic Secret에 따라 아래와 같은 CRD를 가집니다.
Static Secrets 는 운영자가 Vault의 단일 Static Secret을 단일 쿠버네티스 Secret으로 동기화하는 데 필요한 구성입니다.
- CRD: VaultStaticSecret
Static Secret에 대한 상세 실습은 아래 문서를 참고하실 수 있습니다.
https://developer.hashicorp.com/vault/tutorials/kubernetes/vault-secrets-operator
Dynamic Secrets는 Vault의 Dynamic Secret Engine을 활용하여 동적으로 변경되는 Secrets을 K8s Secrets에 동기화할 수 있습니다. 여기에 지원되는 시크릿 엔진에는 DB Credentials, Cloud Credentials(AWS, Azure, GCP 등)이 있습니다.
- CRD: VaultDynamicSecret
본 실습에서 Dynamic Secret에 대해서 실습을 이어 나가겠습니다.
먼저 VSO 배포를 위한 Chart Values 파일 작성합니다.
cat > vault-operator-values.yaml <<EOF
defaultVaultConnection:
enabled: true
address: "http://vault.vault.svc.cluster.local:8200"
skipTLSVerify: false
controller:
manager:
clientCache:
persistenceModel: direct-encrypted
storageEncryption:
enabled: true
mount: k8s-auth-mount
keyName: vso-client-cache
transitMount: demo-transit
kubernetes:
role: auth-role-operator
serviceAccount: vault-secrets-operator-controller-manager
tokenAudiences: ["vault"]
EOF
value.yaml을 바탕으로 Vault Secret Operator를 생성합니다.
helm install vault-secrets-operator hashicorp/vault-secrets-operator \
-n vault-secrets-operator-system \
--create-namespace \
--values vault-operator-values.yaml
# 확인
kubectl get po,svc -n vault-secrets-operator-system
NAME READY STATUS RESTARTS AGE
pod/vault-secrets-operator-controller-manager-7f67cd89fd-qmsb2 2/2 Running 0 39s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/vault-secrets-operator-metrics-service ClusterIP 10.96.156.114 <none> 8443/TCP 39s
kubectl describe pod -n vault-secrets-operator-system
...
Service Account: vault-secrets-operator-controller-manager
...
Containers:
kube-rbac-proxy:
Container ID: containerd://6500b98aa55510c0b1a5950c8c91e3b6c2b4ea312f5c7b0b5ce843d615a543ac
Image: quay.io/brancz/kube-rbac-proxy:v0.18.1
Image ID: quay.io/brancz/kube-rbac-proxy@sha256:e6a323504999b2a4d2a6bf94f8580a050378eba0900fd31335cf9df5787d9a9b
Port: 8443/TCP
Host Port: 0/TCP
Args:
--secure-listen-address=0.0.0.0:8443
--upstream=http://127.0.0.1:8080/
--logtostderr=true
--v=0
...
manager:
Container ID: containerd://c65fc319d184fe9145281db7bdfa88e9169ee4ddc86db42ded2a61b5194ca7c0
Image: hashicorp/vault-secrets-operator:0.10.0
Image ID: docker.io/hashicorp/vault-secrets-operator@sha256:3ee9b27677077cb3324ad02feb68fb7c25cfe381cb8ab5f940eee23c16f8c9a8
Port: <none>
Host Port: <none>
Command:
/vault-secrets-operator
Args:
--health-probe-bind-address=:8081
--metrics-bind-address=127.0.0.1:8080
--leader-elect
--client-cache-persistence-model=direct-encrypted
--global-vault-auth-options=allow-default-globals
--backoff-initial-interval=5s
--backoff-max-interval=60s
--backoff-max-elapsed-time=0s
--backoff-multiplier=1.50
--backoff-randomization-factor=0.50
--zap-log-level=info
--zap-time-encoding=rfc3339
--zap-stacktrace-level=panic
...
# CRD 확인
kubectl get crd |grep vault
hcpvaultsecretsapps.secrets.hashicorp.com 2025-04-12T15:48:09Z
vaultauthglobals.secrets.hashicorp.com 2025-04-12T15:48:09Z
vaultauths.secrets.hashicorp.com 2025-04-12T15:48:10Z
vaultconnections.secrets.hashicorp.com 2025-04-12T15:48:10Z
vaultdynamicsecrets.secrets.hashicorp.com 2025-04-12T15:48:10Z
vaultpkisecrets.secrets.hashicorp.com 2025-04-12T15:48:10Z
vaultstaticsecrets.secrets.hashicorp.com 2025-04-12T15:48:10Z
# vault Auth 확인
kubectl get vaultauth -n vault-secrets-operator-system vault-secrets-operator-default-transit-auth -o jsonpath='{.spec}' | jq
{
"kubernetes": {
"audiences": [
"vault"
],
"role": "auth-role-operator",
"serviceAccount": "vault-secrets-operator-controller-manager",
"tokenExpirationSeconds": 600
},
"method": "kubernetes",
"mount": "k8s-auth-mount",
"storageEncryption": {
"keyName": "vso-client-cache",
"mount": "demo-transit"
},
"vaultConnectionRef": "default"
}
# vault Connnection 확인
kubectl get vaultconnection -n vault-secrets-operator-system default -o jsonpath='{.spec}' | jq
{
"address": "http://vault.vault.svc.cluster.local:8200",
"skipTLSVerify": false
}
실습에서는 Dynamic Secret을 테스트 하기 위해서 데이터베이스의 credentials을 dynamic Secret으로 사용합니다. 예제 데이터베이스인 PostgreSQL 설치합니다.
helm repo add bitnami https://charts.bitnami.com/bitnami
helm upgrade --install postgres bitnami/postgresql \
--namespace postgres \
--create-namespace \
--set auth.audit.logConnections=true \
--set auth.postgresPassword=secret-pass
# 확인
kubectl get po -n postgres
NAME READY STATUS RESTARTS AGE
postgres-postgresql-0 1/1 Running 0 108s
# psql 로그인 확인
kubectl exec -it -n postgres postgres-postgresql-0 -- sh -c 'PGPASSWORD=secret-pass psql -U postgres -h localhost'
psql (17.4)
Type "help" for help.
postgres=#
kubectl exec -it -n postgres postgres-postgresql-0 -- sh -c "PGPASSWORD=secret-pass psql -U postgres -h localhost -c '\l'"
List of databases
Name | Owner | Encoding | Locale Provider | Collate | Ctype | Locale | ICU Rules | Access privileges
-----------+----------+----------+-----------------+-------------+-------------+--------+-----------+-----------------------
postgres | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | |
template0 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
template1 | postgres | UTF8 | libc | en_US.UTF-8 | en_US.UTF-8 | | | =c/postgres +
| | | | | | | | postgres=CTc/postgres
(3 rows)
Vault Database Secret Engine 을 활성화 합니다. 아래 과정에서 보면 알 수 있듯이, Database Secret Engine을 통해서 PostgreSQL 연결 정보를 등록하고, 이 연결 정보를 사용할 수 있는 Role을 지정합니다.
# demo-db라는 경로로 Database Secret Engine을 활성화
vault secrets enable -path=demo-db database
# PostgreSQL 연결 정보 등록
# 해당 과정은 postgres가 정상적으로 동작 시 적용 가능
# allowed_roles: 이후 설정할 Role 이름 지정
vault write demo-db/config/demo-db \
plugin_name=postgresql-database-plugin \
allowed_roles="dev-postgres" \
connection_url="postgresql://{{username}}:{{password}}@postgres-postgresql.postgres.svc.cluster.local:5432/postgres?sslmode=disable" \
username="postgres" \
password="secret-pass"
위 작업을 통해 postgresql-database-plug을 통해 연결정보를 등록했고, 이를 사용하는데 허용된 Role을 아래와 같이 생성합니다.
해당 Role 사용 시 Vault가 동적으로 사용자 계정과 비밀번호를 생성 가능하게 됩니다. 이때 실제 postgreSQL에 대한 연결정보는 Vault의 Database Secret Engine에 저장되고, Vault가 중간에 동적으로 DB 연결에 필요한 계정/비밀번호를 생성하게 됩니다. 이를 통해 postgreSQL에 대한 관리 권한을 Vault에 위임하다고 볼 수 있습니다.
# DB 사용자 동적 생성 Role 등록
vault write demo-db/roles/dev-postgres \
db_name=demo-db \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT ALL PRIVILEGES ON DATABASE postgres TO \"{{name}}\";" \
revocation_statements="REVOKE ALL ON DATABASE postgres FROM \"{{name}}\";" \
backend=demo-db \
name=dev-postgres \
default_ttl="1m" \
max_ttl="1m"
애플리케이션에서 사용할 Policy와 Role을 생성합니다.
# Policy 설정: DB 자격증명 읽기 권한
# demo-db/creds/dev-postgres 경로에 대한 read 권한 부여
# 추후 Kubernetes 서비스 어카운트(demo-dynamic-app)에 이 정책을 연결해서 자격증명 요청 가능
vault policy write demo-auth-policy-db - <<EOF
path "demo-db/creds/dev-postgres" {
capabilities = ["read"]
}
EOF
# Role 생성: 실행 중인 애플리케이션이 Vault로부터 DB 자격증명(동적 사용자)을 받아올 수 있도록 권한을 연결을 위한 설정
vault write auth/kubernetes/role/auth-role \
bound_service_account_names="demo-dynamic-app" \
bound_service_account_namespaces="demo-ns" \
policies="demo-auth-policy-db" \
token_ttl=0 \
token_period=120 \
audience=vault
아래의 과정은 쿠버네티스의 토큰을 받아오는 과정을 캐싱하기 위해서 Transit Secret Engine 설정하는 과정입니다.
# kubectl exec --stdin=true --tty=true vault-0 -n vault -- /bin/sh
----------------------------------------------------------------
# Transit Secret Engine 활성화
# transit 엔진을 demo-transit 경로로 활성화.
# 데이터를 저장하지 않고 암복호화 기능만 제공하는 Vault의 기능
# 클라이언트 캐시는 리더십 변경 시에도 Vault 토큰 및 동적 비밀 임대를 계속 추적하고 갱신할 수 있으므로 원활한 업그레이드를 지원합니다
## Vault 서버에 클라이언트 캐시를 저장하고 암호화할 수 있습니다.
## Vault 동적 비밀을 사용하는 경우 클라이언트 캐시를 영구적으로 저장하고 암호화하는 것이 좋습니다.
## 이렇게 하면 재시작 및 업그레이드를 통해 동적 비밀 임대가 유지됩니다.
vault secrets enable -path=demo-transit transit
vault secrets list
Path Type Accessor Description
---- ---- -------- -----------
cubbyhole/ cubbyhole cubbyhole_baa07f5b per-token private secret storage
demo-db/ database database_3608860f n/a
demo-transit/ transit transit_e5ab4c20 n/a
identity/ identity identity_d7905b56 identity store
secret/ kv kv_d4b43c42 n/a
sys/ system system_4a32a805 system endpoints used for control, policy and debugging
# vso-client-cache라는 키를 생성
# 이 키는 VSO가 암복호화 시 사용할 암호화 키 역할
vault write -force demo-transit/keys/vso-client-cache
# vso-client-cache 키에 대해 암호화(encrypt), 복호화(decrypt)를 허용하는 정책 생성
vault policy write demo-auth-policy-operator - <<EOF
path "demo-transit/encrypt/vso-client-cache" {
capabilities = ["create", "update"]
}
path "demo-transit/decrypt/vso-client-cache" {
capabilities = ["create", "update"]
}
EOF
# Vault Secrets Operator가 사용하는 ServiceAccount에 위 정책을 바인딩
# vso가 Vault에 로그인할 때 사용할 수 있는 JWT 기반 Role 설정
# 해당 Role을 통해 Operator는 Transit 엔진을 이용한 암복호화 API 호출 가능
vault write auth/kubernetes/role/auth-role-operator \
bound_service_account_names=vault-secrets-operator-controller-manager \
bound_service_account_namespaces=vault-secrets-operator-system \
token_ttl=0 \
token_period=120 \
token_policies=demo-auth-policy-operator \
audience=vault
vault read auth/kubernetes/role/auth-role-operator
Key Value
--- -----
alias_name_source serviceaccount_uid
audience vault
bound_service_account_names [vault-secrets-operator-controller-manager]
bound_service_account_namespace_selector n/a
bound_service_account_namespaces [vault-secrets-operator-system]
token_bound_cidrs []
token_explicit_max_ttl 0s
token_max_ttl 0s
token_no_default_policy false
token_num_uses 0
token_period 2m
token_policies [demo-auth-policy-operator]
token_ttl 0s
token_type default
이제 샘플 애플리케이션 YAML 작성 및 배포하겠습니다.
- demo-ns 네임스페이스 생성하고, 폴더를 생성합니다.
vault-auth-dynamic.yaml
:- 애플리케이션이 Vault에 인증하기 위한 ServiceAccount 및 VaultAuth 리소스
kubectl create ns demo-ns mkdir vso-dynamic cd vso-dynamic cat > vault-auth-dynamic.yaml <<EOF apiVersion: v1 kind: ServiceAccount metadata: namespace: demo-ns name: demo-dynamic-app --- apiVersion: secrets.hashicorp.com/v1beta1 kind: VaultAuth metadata: name: dynamic-auth namespace: demo-ns spec: method: kubernetes mount: kubernetes # kubernetes auth 이름 kubernetes: role: auth-role serviceAccount: demo-dynamic-app audiences: - vault EOF
app-secret.yaml
:- Spring App에서 PostgreSQL에 접속할 때 사용할 해당 시크릿에 username/password을 동적으로 생성
cat > app-secret.yaml <<EOF apiVersion: v1 kind: Secret metadata: name: vso-db-demo namespace: demo-ns EOF
vault-dynamic-secret.yaml
- Vault에서 동적으로 PostgreSQL 접속정보 생성하고 K8s Secret에 저장
cat > vault-dynamic-secret.yaml <<EOF apiVersion: secrets.hashicorp.com/v1beta1 kind: VaultDynamicSecret metadata: name: vso-db-demo namespace: demo-ns spec: refreshAfter: 25s mount: demo-db path: creds/dev-postgres destination: name: vso-db-demo # 대상 secret을 지정 create: true overwrite: true vaultAuthRef: dynamic-auth # VaultAuth 을 참조함 rolloutRestartTargets: # secret이 변경될때 rollout되는 디플로이먼트를 지정 - kind: Deployment name: vaultdemo EOF
app-spring-deploy.yaml
:- DB 접속 테스트를 위한 Spring App
애플리케이션 배포를 위해서 해당 폴더에서cat > app-spring-deploy.yaml <<EOF apiVersion: apps/v1 kind: Deployment metadata: name: vaultdemo namespace: demo-ns labels: app: vaultdemo spec: replicas: 1 selector: matchLabels: app: vaultdemo template: metadata: labels: app: vaultdemo spec: volumes: - name: secrets secret: secretName: "vso-db-demo" containers: - name: vaultdemo image: hyungwookhub/vso-spring-demo:v5 imagePullPolicy: IfNotPresent env: - name: DB_PASSWORD valueFrom: secretKeyRef: name: "vso-db-demo" key: password - name: DB_USERNAME valueFrom: secretKeyRef: name: "vso-db-demo" key: username - name: DB_HOST value: "postgres-postgresql.postgres.svc.cluster.local" - name: DB_PORT value: "5432" - name: DB_NAME value: "postgres" ports: - containerPort: 8088 volumeMounts: - name: secrets mountPath: /etc/secrets readOnly: true --- apiVersion: v1 kind: Service metadata: name: vaultdemo namespace: demo-ns spec: ports: - name: vaultdemo port: 8088 targetPort: 8088 nodePort: 30003 selector: app: vaultdemo type: NodePort EOF
kubectl apply -f .
를 수행합니다.
실행된 파드와 서비스를 확인합니다.
kubectl get po,svc -n demo-ns
NAME READY STATUS RESTARTS AGE
pod/vaultdemo-65766f6679-sccdh 1/1 Running 0 4s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/vaultdemo NodePort 10.96.46.22 <none> 8088:30003/TCP 7m37s
이제 실제 동작을 UI(http://localhost:30003)에서 확인합니다.
샘플 애플리케이션에서 노출된 연결 정보를 통해 데이터베이스 접속이 성공하는 것을 알 수 있습니다.
세부적인 내용은 CLI를 통해서 살펴보겠습니다.
# Secret이 변경되고, 적용을 위해서 파드가 42~45초 사이로 재생성됨
while true; do kubectl get pod -n demo-ns ; echo; kubectl get secret -n demo-ns vso-db-demo -oyaml |egrep "username|password"; date; sleep 30 ; echo ; done
NAME READY STATUS RESTARTS AGE
vaultdemo-7ccfbc4bc5-sv24l 1/1 Running 0 8s
password: M1h5dkctVXFEZm05TnVvYm1Jamc=
username: di1rdWJlcm5ldC1kZXYtcG9zdC1kOXZWSnBwWlF5S0RIa2V3VjVEUi0xNzQ0NDc3MDc4
Sun Apr 13 01:58:07 KST 2025
NAME READY STATUS RESTARTS AGE
vaultdemo-7ccfbc4bc5-sv24l 1/1 Running 0 39s
password: M1h5dkctVXFEZm05TnVvYm1Jamc=
username: di1rdWJlcm5ldC1kZXYtcG9zdC1kOXZWSnBwWlF5S0RIa2V3VjVEUi0xNzQ0NDc3MDc4
Sun Apr 13 01:58:38 KST 2025
NAME READY STATUS RESTARTS AGE
vaultdemo-685f97956f-5xn9d 1/1 Running 0 28s
password: d0YtdzJMSXZocngxbnppLUU1SFo=
username: di1rdWJlcm5ldC1kZXYtcG9zdC1JNmVsSmxXRHdvY3ZUa0NKUXdtVy0xNzQ0NDc3MTIw
Sun Apr 13 01:59:08 KST 2025
# 실제 postgresql 에 사용자 정보 확인 (계속 추가되고 있음)
# kubectl exec -it -n postgres postgres-postgresql-0 -- sh -c "PGPASSWORD=secret-pass psql -U postgres -h localhost -c '\du'"
List of roles
Role name | Attributes
-----------------------------------------------------+------------------------------------------------------------
postgres | Superuser, Create role, Create DB, Replication, Bypass RLS
v-kubernet-dev-post-1UGLywrLiJTZaaEf8miA-1744476863 | Password valid until 2025-04-12 16:55:28+00
v-kubernet-dev-post-6UvLI5FPiUF9fMY7QNEp-1744476652 | Password valid until 2025-04-12 16:51:57+00
v-kubernet-dev-post-7mJgWsGrFamOFToYkZLX-1744476819 | Password valid until 2025-04-12 16:54:44+00
v-kubernet-dev-post-9heCLQZOM1vEr5F61aC2-1744476733 | Password valid until 2025-04-12 16:53:18+00
v-kubernet-dev-post-BTeXCNTCA2sRfAXUs84e-1744476390 | Password valid until 2025-04-12 16:47:35+00
v-kubernet-dev-post-BiTwN5EhluVL7vqbnMng-1744476390 | Password valid until 2025-04-12 16:47:35+00
v-kubernet-dev-post-F23WnObMJvf1b3EXjMX8-1744476909 | Password valid until 2025-04-12 16:56:14+00
v-kubernet-dev-post-F66ZR0rV2R9yaoelOtBr-1744476566 | Password valid until 2025-04-12 16:50:31+00
v-kubernet-dev-post-I6elJlWDwocvTkCJQwmW-1744477120 | Password valid until 2025-04-12 16:59:45+00
v-kubernet-dev-post-Jh15Ui20lrcxuFo8yH1b-1744476432 | Password valid until 2025-04-12 16:48:17+00
v-kubernet-dev-post-T8gKAH6Z2h7hPJeDpVs6-1744476521 | Password valid until 2025-04-12 16:49:46+00
...
# 로그 확인
kubectl stern -n demo-ns -l app=vaultdemo
...
kubectl stern -n vault vault-0
...
vault-0 vault 2025-04-10T08:32:09.484Z [INFO] expiration: revoked lease: lease_id=demo-db/creds/dev-postgres/Ph1sg8efnqssOU5FVIuAqhMW
vault-0 vault 2025-04-10T08:32:54.229Z [INFO] expiration: revoked lease: lease_id=demo-db/creds/dev-postgres/XKkHZ65ZYLeILvo8GqWfE7F3
vault-0 vault 2025-04-10T08:33:36.804Z [INFO] expiration: revoked lease: lease_id=demo-db/creds/dev-postgres/rk1KUFsV7SjZPNFsCqCHGZVx
...
실습을 마무리하고 kind 클러스터를 삭제합니다.
# 클러스터 삭제
kind delete cluster --name myk8s
# 확인
docker ps
cat ~/.kube/config
마무리
쿠버네티스 환경에서 Setcret을 사용하기 위해서 Vault를 활용하는 방안을 살펴보았습니다. 실습은 간략한 Static Secret과 Dynamic Secret을 예제로 살펴보았지만, Vault를 사용해 Secret 관리를 위임하는 다양한 사례가 있을 수 있습니다.
Vault를 활용하는 방식에서 Sidecar injector나 CSI Driver를 사용하는 방식은 간단하지만 기존 워크로드의 변경이 있어야 하기 때문에 다소 불편할 수 있습니다. 한편 VSO를 사용하는 방법은 사용 방식은 복잡하지만, 개발자 입장에서는 기존 쿠버네티스의 Secret을 사용하는 인터페이스를 그대로 사용할 수 있다는 장점이 있었습니다.
그럼 이번 포스트를 마무리 하겠습니다.
'Kubernetes' 카테고리의 다른 글
Jenkins와 Argo CD를 활용한 Kubernetes 환경 CI/CD 구성 (0) | 2025.03.30 |
---|---|
CNI(Container Network Interface)란? (0) | 2025.02.19 |
쿠버네티스에 containerd 를 사용하는 윈도우 워커노드 추가 (with Calico CNI) (0) | 2022.07.12 |
쿠버네티스 윈도우 워커 노드 추가 (with Calico CNI) (2) | 2022.03.14 |
Kubernetes 업그레이드 (K8S v1.21.x → v1.22.x) (0) | 2022.02.05 |