
금일 작성하는 포스팅은 8주차 K8S CI/CD 실습시 사용했던 환경을 그대로 사용할 예정입니다.
HashiCorp Vault는 신원 기반의 시크릿 및 암호화 관리 시스템으로, 토큰, 비밀번호, 인증서, 암호화 키 등 민감한 정보를 안전하게 저장하고 관리합니다.
주요 기능
출처 : AEWS 3기 스터디
HashiCorp Vault는 토큰 기반으로 작동하며, 각 토큰은 클라이언트의 정책(Policy)과 연결되어 있어 접근 권한을 제어합니다. 정책은 경로(Path) 기반으로 설정되며, 클라이언트가 어떤 작업을 할 수 있는지 정의합니다.
Vault의 핵심 워크플로우는 다음과 같이 구성됩니다
출처 : AEWS 3기 스터디
현대 기업들이 겪는 자격 증명 관리의 보안 위험과 복잡성을 해결하기 위해, HashiCorp Vault는 모든 시크릿을 중앙 집중화하여 안전하게 관리하고, 인증·인가·감사 로그를 통해 접근을 제어합니다.
Vault의 주요 기능은 다음과 같습니다


kubectl create namespace vault
kubectl get all --namespace vault
helm repo add hashicorp https://helm.releases.hashicorp.com
helm search repo hashicorp/vault 
cat <<EOF > override-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
EOFhelm upgrade vault hashicorp/vault -n vault -f override-values.yaml --installkubens vault
k get pods,svc,pvc 
kubectl exec -ti vault-0 -- vault status 
cat <<EOF > init-unseal.sh
#!/bin/bash
# Vault Pod 이름
VAULT_POD="vault-0"
# Vault 명령 실행
VAULT_CMD="kubectl exec -ti \$VAULT_POD -- 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 
kubectl exec -ti vault-0 -- vault status 
token : Unseal 등록시 생성되었던 Root Token을 입력한다.
vault-root-token.txt 에서도 확인이 가능

brew tap hashicorp/tap
brew install hashicorp/tap/vaultvault --version 
export VAULT_ADDR='http://localhost:30000'vault login 
vault secrets enable -path=secret kv-v2 
vault kv put secret/sampleapp/config \
username="demo" \
password="p@ssw0rd" 
vault kv get secret/sampleapp/config 


출처: HashiCorp 공식 블로그
| 요소 | 설명 |
|---|---|
| Sidecar Injector | Vault Agent 컨테이너들을 Pod에 자동 삽입 |
| Vault Agent Init Container | 최초 Vault 인증 및 토큰 발급 담당 |
| Vault Agent Sidecar Container | 시크릿을 Vault에서 가져오고 공유 |
| Kubernetes Auth | Vault가 서비스 어카운트로부터 받은 JWT로 인증 수행 |
| Secret Injection | 시크릿이 /vault/secrets 등의 경로로 마운트됨 |
출처: AEWS 스터디 3기
app_pol)을 적용한 Vault Token을 반환함.
출처: HashiCorp 공식 블로그
vault auth enable approle || echo "AppRole already enabled"
vault auth list 
vault policy write sampleapp-policy - <<EOF
path "secret/data/sampleapp/*" {
capabilities = ["read"]
}
EOF 
vault write auth/approle/role/sampleapp-role \
token_policies="sampleapp-policy" \
secret_id_ttl="4h" \
token_ttl="4h" \
token_max_ttl="7h" ROLE_ID=$(vault read -field=role_id auth/approle/role/sampleapp-role/role-id)
SECRET_ID=$(vault write -f -field=secret_id auth/approle/role/sampleapp-role/secret-id)
mkdir -p approle-creds
echo "$ROLE_ID" > approle-creds/role_id.txt
echo "$SECRET_ID" > approle-creds/secret_id.txt
kubectl create secret generic vault-approle -n vault \
--from-literal=role_id="${ROLE_ID}" \
--from-literal=secret_id="${SECRET_ID}" \
--save-config \
--dry-run=client -o yaml | kubectl apply -f -cat <<EOF | kubectl create configmap vault-agent-config -n vault --from-file=agent-config.hcl=/dev/stdin --dry-run=client -o yaml | kubectl apply -f -
vault {
address = "http://vault.vault.svc:8200"
}
auto_auth {
method "approle" {
config = {
role_id_file_path = "/etc/vault/approle/role_id"
secret_id_file_path = "/etc/vault/approle/secret_id"
remove_secret_id_file_after_reading = false
}
}
sink "file" {
config = {
path = "/etc/vault-agent-token/token"
}
}
}
template_config {
static_secret_render_interval = "20s"
}
template {
destination = "/etc/secrets/index.html"
contents = <<EOH
<html>
<body>
<p>username: {{ with secret "secret/data/sampleapp/config" }}{{ .Data.data.username }}{{ end }}</p>
<p>password: {{ with secret "secret/data/sampleapp/config" }}{{ .Data.data.password }}{{ end }}</p>
</body>
</html>
EOH
}
EOF
kubectl apply -n vault -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-vault-demo
spec:
replicas: 1
selector:
matchLabels:
app: nginx-vault-demo
template:
metadata:
labels:
app: nginx-vault-demo
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
volumeMounts:
- name: html-volume
mountPath: /usr/share/nginx/html
- name: vault-agent-sidecar
image: hashicorp/vault:latest
args:
- "agent"
- "-config=/etc/vault/agent-config.hcl"
volumeMounts:
- name: vault-agent-config
mountPath: /etc/vault
- name: vault-approle
mountPath: /etc/vault/approle
- name: vault-token
mountPath: /etc/vault-agent-token
- name: html-volume
mountPath: /etc/secrets
volumes:
- name: vault-agent-config
configMap:
name: vault-agent-config
- name: vault-approle
secret:
secretName: vault-approle
- name: vault-token
emptyDir: {}
- name: html-volume
emptyDir: {}
EOFkubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort
selector:
app: nginx-vault-demo
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30001 # Kind에서 설정한 Port
EOF
kubectl get pod -l app=nginx-vault-demo 
kubectl describe pod -l app=nginx-vault-demo 
볼륨 마운트 확인
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- ls -l /etc/vault-agent-token

kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/vault-agent-token/token ; echo

kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- ls -al /etc/vault

kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/vault/agent-config.hcl

kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- ls -al /etc/vault/approle
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/vault/approle/role_id ; echo
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/vault/approle/secret_id ; echo

kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- ls -l /etc/secrets

kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/secrets/index.html

kubectl exec -it deploy/nginx-vault-demo -c nginx -- cat /usr/share/nginx/html/index.html

kubectl get mutatingwebhookconfigurations.admissionregistration.k8s.io 





출처 : hashicorp developer
RoleName을 알고 있고, 이를 기반으로 Vault에 SID를 요청합니다.Jenkins UI 접속
상단 메뉴에서 Manage Jenkins → Plugins
Available 탭에서 Vault 검색
HashiCorp Vault Plugin 설치 후 Jenkins 재시작

ROLE_ID=$(vault read -field=role_id auth/approle/role/sampleapp-role/role-id)
SECRET_ID=$(vault write -f -field=secret_id auth/approle/role/sampleapp-role/secret-id)
echo "ROLE_ID: $ROLE_ID"
echo "SECRET_ID: $SECRET_ID"

종류: Vault AppRole Credential
Role ID & Secret ID 입력 → 생성해놓은 변수 또는 파일참고
ID는 기억하기 쉬운 이름으로 지정 (vault-approle-creds )

Jenkins UI → New Item → Pipeline 선택
jenkins-vault-kv 입력 후 생성

Jenkinsfile 작성
pipeline {
agent any
environment {
VAULT_ADDR = 'http://192.168.35.185:30000' // 실제 Vault 주소로 변경!!!
}
stages {
stage('Read Vault Secret') {
steps {
withVault([
vaultSecrets: [
[
path: 'secret/sampleapp/config',
engineVersion: 2,
secretValues: [
[envVar: 'USERNAME', vaultKey: 'username'],
[envVar: 'PASSWORD', vaultKey: 'password']
]
]
],
configuration: [
vaultUrl: "${VAULT_ADDR}",
vaultCredentialId: 'vault-approle-creds'
]
]) {
sh '''
echo "Username from Vault: $USERNAME"
echo "Password from Vault: $PASSWORD"
'''
script {
echo "Username (env): ${env.USERNAME}"
echo "Password (env): ${env.PASSWORD}"
}
}
}
}
}
}
빌드 실행 : 마스킹 처리하여 결과를 보여줌

kubectl apply -f - <<EOF
kind: Secret
apiVersion: v1
metadata:
name: argocd-vault-plugin-credentials
namespace: argocd
type: Opaque
stringData:
VAULT_ADDR: "http://vault.vault:8200"
AVP_TYPE: "vault"
AVP_AUTH_TYPE: "approle"
AVP_ROLE_ID: 0192cd70-2366-efbd-f219-7396342bbe9f #ROLE_ID
AVP_SECRET_ID: 88cbb2bb-8a34-f8a1-56e0-2ac6d16a7cf5 #SECRET_ID
EOFgit clone https://github.com/hyungwook0221/argocd-vault-plugin.git
cd argocd-vault-plugin/manifests/cmp-sidecar
# argocd 네임스페이스 설정
kubens argocd
# 생성될 메니페스트 파일에 대한 확인
kubectl kustomize .
# -k 옵션으로 kusomize 실행
kubectl apply -n argocd -k . 
kubectl apply -n argocd -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: demo
namespace: argocd
spec:
destination:
namespace: argocd
server: https://kubernetes.default.svc
project: default
source:
path: infra/helm
repoURL: https://github.com/hyungwook0221/spring-boot-debug-app
targetRevision: main
plugin:
name: argocd-vault-plugin-helm
env:
- name: HELM_ARGS
value: -f new-values.yaml
syncPolicy:
automated:
prune: true
selfHeal: true
EOF



출처 : HashiCorp Blog
Vault Secrets Operator는 CRD의 변화를 감지해 Vault 등에서 Kubernetes Secret으로 데이터를 동기화하며, 변경 사항이 생길 때마다 이를 지속적으로 반영해 애플리케이션이 최신 비밀 데이터에 접근할 수 있도록 합니다.
출처 : AEWS 스터디 3기
cat <<EOF > vault-operator-values.yaml
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
cat vault-operator-values.yaml
helm install vault-secrets-operator hashicorp/vault-secrets-operator \
-n vault-secrets-operator-system \
--create-namespace \
--values vault-operator-values.yaml

kubectl create ns postgres
helm repo add bitnami https://charts.bitnami.com/bitnami
helm upgrade --install postgres bitnami/postgresql \
--namespace postgres \
--set auth.audit.logConnections=true \
--set auth.postgresPassword=secret-pass

kubectl exec --stdin=true --tty=true vault-0 -n vault -- /bin/shvault login 
vault auth enable -path k8s-auth-mount kubernetes 
vault write auth/k8s-auth-mount/config \
kubernetes_host="https://kubernetes.default.svc:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" 
vault read auth/k8s-auth-mount/config 
실행중인 애플리케이션이 Vault로부터 DB 자격증명을 받아올수 있도록 권한 연결
vault write auth/k8s-auth-mount/role/auth-role \
bound_service_account_names=demo-dynamic-app \
bound_service_account_namespaces=demo-ns \
token_ttl=0 \
token_period=120 \
token_policies=demo-auth-policy-db \
audience=vault

vault secrets enable -path=demo-db database 
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" 
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" 
vault policy write demo-auth-policy-db - <<EOF
path "demo-db/creds/dev-postgres" {
capabilities = ["read"]
}
EOF

캐싱시 암호화 처리를 위하여 반드시 구성 필요
vault secrets enable -path=demo-transit transit 
vso-client-cache 키 생성 : VSO 암복호화 시 사용할 암호화 키 역활vault write -force demo-transit/keys/vso-client-cache 
vso-client-cache 에 대한 암호화/복호화 허용하는 정책 생성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 
vso가 Vault에 로그인할 때 사용할 수 있는 JWT 기반 Role 설정
해당 Role을 통해 Operator는 Transit 엔진을 이용한 암복호화 API 호출 가능
vault write auth/k8s-auth-mount/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/k8s-auth-mount/role/auth-role-operator 
demo-ns namespace 생성kubectl create ns demo-nsmkdir vso-dynamic
cd vso-dynamicvault-auth-dynamic.yaml 생성cat <<EOF > vault-auth-dynamic.yaml
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: k8s-auth-mount
kubernetes:
role: auth-role
serviceAccount: demo-dynamic-app
audiences:
- vault
EOFapp-secret.yaml 생성cat <<EOF > app-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: vso-db-demo
namespace: demo-ns
EOFvault-dynamic-secret.yaml 생성cat <<EOF > vault-dynamic-secret.yaml
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
create: true
overwrite: true
vaultAuthRef: dynamic-auth
rolloutRestartTargets:
- kind: Deployment
name: vaultdemo
EOFapp-spring-deploy.yamlcat <<EOF > app-spring-deploy.yaml
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: 30005
selector:
app: vaultdemo
type: NodePort
EOFkubectl apply -f vault-auth-dynamic.yaml
kubectl apply -f app-secret.yaml
kubectl apply -f vault-dynamic-secret.yaml
kubectl apply -f app-spring-deploy.yaml 
DB Host : postgres-postgresql.postgres.svc.cluster.local
DB Port : 5432
DB name : postgres
Username, Password 아래 DB_USERNAME, DB_PASSWORD 참고

vault secrets enable -path=pki pki

vault write pki/root/generate/internal \
common_name="hashicorp.local" \
ttl=87600h 
vault write pki/config/urls \
issuing_certificates="http://vault.vault.svc:8200/v1/pki/ca" \
crl_distribution_points="http://vault.vault.svc:8200/v1/pki/crl" 
hashicorp-app.svc.cluster.local 도메인 네임에 대해 PKI 엔진에 요청 허용vault write pki/roles/cert-role \
allowed_domains="hashicorp-app.svc.cluster.local" \
allow_subdomains=true \
allow_bare_domains=true \
allow_any_name=true \
enforce_hostnames=false \
max_ttl="24h" 
vault policy write pki-policy - <<EOF
path "pki/issue/cert-role" {
capabilities = ["update", "read", "list"]
}
EOF

vault write auth/approle/role/cert-role \
token_policies="pki-policy" \
token_ttl="30m" \
token_max_ttl="1h"

vault read -field=role_id auth/approle/role/cert-role/role-id > role_id.txt
vault write -f -field=secret_id auth/approle/role/cert-role/secret-id > secret_id.txt
# 확인
cat role_id.txt
cat secret_id.txt

kubectl create ns webapp

kubectl create secret generic vso-cert-role \
-n webapp \
--from-file=role_id=role_id.txt \
--from-file=id=secret_id.txt

kubectl create -f - <<EOF
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: pki-auth
namespace: webapp
spec:
method: appRole
mount: approle
appRole:
roleId: ff2a2786-b998-c61e-ee94-a203dedc70c1 # RoleID
secretRef: vso-cert-role
EOF

kubectl create -f - <<EOF
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultPKISecret
metadata:
name: hashicorp-tls
namespace: webapp
spec:
vaultAuthRef: pki-auth
mount: pki
role: cert-role
commonName: hashicorp-app.svc.cluster.local
ttl: 1m
destination:
create: true
name: vso-pki-cert
rolloutRestartTargets:
- kind: Deployment
name: nginx-vault-ssl
EOF 
kubectl get secret vso-pki-cert -n webapp -o json | jq -r .data._raw | base64 -d 
kubectl krew install view-secretkubectl view-secret -n webapp vso-pki-cert --all 
kubectl view-secret -n webapp vso-pki-cert expiration 
date -r 1744267831 
kubectl apply -f - <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
name: nginx-ssl-config
namespace: webapp
data:
nginx.conf: |
events {}
http {
server {
listen 443 ssl;
server_name localhost;
ssl_certificate /etc/nginx/tls/certificate;
ssl_certificate_key /etc/nginx/tls/private_key;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-vault-ssl
namespace: webapp
spec:
replicas: 1
minReadySeconds: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
maxSurge: 1
selector:
matchLabels:
app: nginx-vault
template:
metadata:
labels:
app: nginx-vault
spec:
initContainers:
- name: generate-expiration-html
image: alpine:latest
command: ["/bin/sh", "-c"]
args:
- |
apk add --no-cache openssl > /dev/null
expiry=\$(openssl x509 -enddate -noout -in /certs/certificate | cut -d= -f2)
echo "<html><head><meta http-equiv='refresh' content='10'></head><body><h1>Certificate Expiration: \$expiry</h1></body></html>" > /output/index.html
volumeMounts:
- name: tls
mountPath: /certs
readOnly: true
- name: html
mountPath: /output
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 443
volumeMounts:
- name: tls
mountPath: /etc/nginx/tls
readOnly: true
- name: html
mountPath: /usr/share/nginx/html
- name: nginx-conf
mountPath: /etc/nginx/nginx.conf
subPath: nginx.conf
volumes:
- name: tls
secret:
secretName: vso-pki-cert
- name: html
emptyDir: {}
- name: nginx-conf
configMap:
name: nginx-ssl-config
---
apiVersion: v1
kind: Service
metadata:
name: nginx-vault-service
namespace: webapp
spec:
selector:
app: nginx-vault
ports:
- protocol: TCP
port: 443
targetPort: 443
nodePort: 30004
type: NodePort
EOF

서비스를 확인하면 다음과 같이 인증서가 배포되어 있습니다.
단 공인된 인증서가 아니므로 주의요함 이 나타남
