Vault 개요
Vault에 대해 간단히 알아보고, 쿠버네티스 환경에서 시크릿을 어떻게 안전하게 관리할 수 있는지, 그리고 CI/CD 도구(Jenkins, ArgoCD)와 어떻게 연동할 수 있는지 실습을 통해 확인해보자.
Vault란?
Vault는 HashiCorp에서 개발한 보안 비밀 관리 도구로,
API Key, 비밀번호, 인증서 같은 민감한 정보를 안전하게 저장하고 관리할 수 있도록 지원한다.
- 역할 기반 접근 제어(RBAC)를 통해 세밀한 권한 관리 가능
- 정적/동적 시크릿 관리, 자동 인증서 발급, 키 로테이션 등 다양한 기능 제공
- 오픈소스와 엔터프라이즈 버전이 있으며, 다양한 클라우드 및 온프레미스 환경과 통합 가능
Vault는 단순한 키-값 저장소를 넘어, 시크릿의 수명 주기를 자동으로 관리하고, 인프라 전반에 보안 정책을 통합하여 일관성 있게 적용할 수 있는 강력한 보안 솔루션이다.
왜 Vault가 필요한가요?
MSA 환경과 수 많은 시스템 간 통신이 이루어지면서, 의도치 않게 민감 정보들이 사용될 수 있다. 특히 CICD에서 사용되는 키들은 중요한 민감정보가 많으므로 더욱도 주의가 필요하다. 최근 GitHub Action tj-actions/changed-files 공격으로 CICD 있어, 보안이 중요하게 대두되고 있다. 시크릿 관리 방법이 더욱이 파편화됨으로 Vault를 사용하여 안전하게 통합 관리하자!
Vault 주요 기능은 다음과 같다.
🔐 비밀 저장 | Key-Value 형태의 시크릿을 저장하며, 저장 전 자동 암호화 처리됨 (Static Secrets) |
⚙️ 동적 비밀 발급 | DB 계정, AWS 키 등을 필요 시점에 동적으로 생성 후 자동 만료 처리 |
🧾 데이터 암호화 | 외부 저장소에 저장할 데이터를 Vault를 통해 암복호화 가능 (데이터는 저장 X) |
⏳ 임대 및 갱신 | 모든 시크릿에 유효 기간(lease)이 있으며, 자동 폐기 및 갱신 API 제공 |
🧹 비밀 폐기 | 특정 비밀 또는 전체 계층의 비밀을 즉시 폐기하여 노출 시 빠르게 대응 가능 |
Vault 설치 및 기본 환경 구성
Helm을 사용하여 HashiCorp Vault를 쿠버네티스 환경에(KinD) 클러스터에 설치해 보자.
Helm 설치 준비
네임스페이스 생성 및 Vault Helm Repo 추가
# Create a Kubernetes namespace.
kubectl create namespace vault
# View all resources in a namespace.
kubectl get all --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 설정 (override-values.yaml)
• tlsDisable
: true로 데모용 TLS 비활성화
• NodePort(30000)를 통해 외부 접속 허용
• injector.enabled = true
: Sidecar 패턴을 위한 Agent Injector도 함께 구성
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
EOF
Helm 배포
# Helm Install 실행
helm upgrade vault hashicorp/vault -n vault -f override-values.yaml --install
# 네임스페이스 변경 : vault
kubens vault
Context "kind-myk8s" modified.
Active namespace is "vault".
# 배포확인
k get pods,svc,pvc
NAME READY STATUS RESTARTS AGE
pod/vault-0 0/1 ContainerCreating 0 11s
pod/vault-agent-injector-56459c7545-9n94t 0/1 ContainerCreating 0 11s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/vault NodePort 10.96.36.121 <none> 8200:30000/TCP,8201:31091/TCP 11s
service/vault-agent-injector-svc ClusterIP 10.96.240.81 <none> 443/TCP 11s
service/vault-internal ClusterIP None <none> 8200/TCP,8201/TCP 11s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/data-vault-0 Bound pvc-6f5739c5-14e9-4a62-bed2-b98fd327bcb1 10Gi RWO standard <unset> 11s
초기화 & Unseal
Vault를 쿠버네티스에 배포하면, vault-0 파드가 0/1 Running 상태로 대기하는 것을 볼 수 있다. 이는 에러가 아니라 정상 동작이다.
Vault는 기본적으로 보안을 위해 초기 상태에서 봉인(Sealed) 되어 있다. 즉, Unseal 과정을 거치기 전까지는 Ready 상태가 되지 않는다.
따라서 다음 단계에서 vault operator init
과 vault operator unseal
명령어(또는 스크립트)를 통해 Unseal을 수행해 줘야 정상적으로 동작한다
kubectl exec -ti vault-0 -- vault status
# 출력 예시
# Sealed: true → 봉인된 상태
init-unseal.sh을 사용하여 Vault Unseal 자동화
• Threshold
는 Vault를 Unseal 하기 위해 필요한 최소 키 개수로, 테스트 환경 시, 1개로 간소화하여 사용
• 생성된 키 값을 파일로 저장해서 나중에 참조
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
vault status
명령을 사용하여 Unseal 되었는지 확인한다 → Sealed=false
kubectl exec -ti vault-0 -- 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-26e23c75
Cluster ID 326975b8-4907-2d62-9da1-f3165d7cc0c6
HA Enabled false
UI에 접속해서 Unseal Key 입력도 가능하다.
이제 Unseal이 마무리되었으므로 Root Token 값을 사용하여 UI에 접속할 수 있다.
Vault CLI 설치
Vault는 웹 UI를 통해 시크릿 관리, 인증 구성 등 다양한 기능을 제공하지만, Vault CLI를 사용하면 로컬 환경에서도 원격 Vault 서버에 직접 명령을 실행하며 제어할 수 있다.
Vault CLI 설치(Arm)
wget <https://releases.hashicorp.com/vault/1.19.1/vault_1.19.1_darwin_arm64.zip>
unzip vault_1.19.1_darwin_arm64.zip
chmod +x vault
rm LICENSE.txt
mv vault /usr/local/bin
vault --version
Vault v1.19.1 (aa75903ec499b2236da9e7bbbfeb7fd16fa4fd9d), built 2025-04-02T15:43:01Z
vault write 명령어 동작 분석해 보기 🤔
Vault write 명령어는 사실 Vault HTTP API의 POST 또는 PUT 요청을 CLI에서 쉽게 사용할 수 있도록 만들어진 래퍼(wrapper) 명령어이라고 한다. 내부적으로는 Vault의 RESTful API를 호출하는 방식이다
따라서, Vault API 명세를 참고하여, write 명령어를 사용하면 용이하다.
Vault 시크릿 생성 해보기
Vault에서 가장 기본적인 시크릿 저장 방식인 KV(Key/Value) Secret Engine을 활성화하고, 샘플 시크릿을 저장해 보자
시크릿 엔진(KV v2)
• KV Version 2는 버전 관리가 가능한 Key/Value 저장소를 제공한다.
• 기본적으로 /data 경로를 사용하며, 메타데이터가 추가되어 버전 롤백 등 기능을 지원하지만, v1에 비해 성능은 약간 저하될 수 있다.
샘플 데이터 추가
• 해당 시크릿은 secret/sampleapp/config 경로에 저장됨
• 실제 저장 위치는 내부적으로 secret/data/sampleapp/config에 매핑됨 (v2 특징)
# 샘플 시크릿 저장
vault kv put secret/sampleapp/config \
username="demo" \
password="p@ssw0rd"
저장 결과 확인
• Vault UI 또는 CLI를 통해 저장된 값을 확인할 수 있다.
vault kv get secret/sampleapp/config
• UI에서도 버전 정보와 메타데이터를 확인할 수 있다.
Vault Sidecar 연동(Vault Agent)
valut-injector 란?
Vault Agent Injector는 Kubernetes Pod에 사이드카 형태로 Vault Agent를 자동으로 주입하는 기능이다. 시크릿 데이터를 주기적으로 가져오고 앱 컨테이너와 공유할 수 있는 구조를 제공한다.
Vault Agent Sidecar 연동 실습
이번 실습에서는 Vault Agent Injector를 통해 Nginx 애플리케이션 파드에 Vault Agent를 사이드카로 연동해 본다.
Vault에서 시크릿을 주기적으로 가져와 템플릿 형태의 정적 파일(index.html)로 렌더링 하고, 이를 Nginx가 서빙하도록 구성해 보자.
시크릿 값을 변경한 뒤, Nginx에서 변경된 파일이 정상 반영되는지도 함께 확인해 본다.
이번 실습은 수동 주입 방식을 기준으로 진행되며, 이후에는 annotation 기반의 자동 주입 방식도 함께 다룰 예정이다.
Vault Agent 설정 파일 작성 및 생성(vault-agent-config.hcl)
이 설정 파일은 Vault Agent가 Sidecar로 실행되면서 AppRole 인증을 하고, Vault의 시크릿을 주기적으로 읽어 와서 템플릿 형식으로 파일을 생성하는 데 사용된다.
템플릿 출력 파일은 보통 Nginx 같은 앱 컨테이너와 공유되는 emptyDir 같은 볼륨에 저장해서 노출시킨다.
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 Agent 설정 파일
vault {
# Vault 서버 주소 (클러스터 내 DNS 이름)
address = "http://vault.vault.svc:8200"
}
# 자동 인증 설정 (AppRole 방식)
auto_auth {
method "approle" {
config = {
# role_id와 secret_id 파일 경로
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" # 발급받은 Vault 토큰 저장 위치
}
}
}
# 템플릿 재렌더링 주기 설정 (시크릿 변경 반영 주기)
template_config {
static_secret_render_interval = "20s" # 20초마다 템플릿 재생성 시도
}
# 시크릿 값을 기반으로 HTML 템플릿 생성
template {
destination = "/etc/secrets/index.html" # 생성된 파일 저장 위치
contents = <<EOH
<html>
<body>
<!-- Vault에서 시크릿을 읽어와 렌더링 -->
<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
샘플 애플리케이션 + Sidecar 배포(수동방식)
Nginx + Vault Agent 생성 그리고 서비스
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: {}
EOF
---
# 서비스
kubectl 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
Jenkins에서 Vault 연동하기
Vault를 연동하면 Jenkins 파이프라인에서 사용하는 환경 변수(Environment Variable)를 외부에 노출하지 않고 안전하게 관리할 수 있다.
단순히 민감한 값을 숨기는 것에 그치지 않고, Jenkinsfile을 수정하지 않고도 Vault에서 중앙 집중적으로 값을 관리하고 변경할 수 있는 유연함이 큰 장점이다
Jenkins Vault Plugin 설치
Vault와 Jenkins를 연동하려면 Vault Plugin for Jenkins을 설치해야 한다.
플러그인 설치
젠킨스 플러그인 메뉴에서 Vault Plugin’을 설치한다.
(Jenkins 관리 > Plugins > Installed plugins
)
Vault 서버 정보 등록
플러그인 설치 후 Jenkins 시스템 설정에서 Vault 서버 정보를 등록한다
(Jenkins 관리 > System > Vault Plugin)
Role ID / Secret ID를 크리덴셜로 등록(AppRole 인증 방식 사용)
( Vault 서버 정보 등록 시, Credential 이 없다면, Add 버튼을 눌러 바로 생성 )
파이프라인 작성 및 실행
Vault에서 시크릿을 읽어와 Jenkins 환경 변수로 사용하는 예제이다.
withVault 블록을 사용해 Vault의 시크릿 값을 Jenkins 환경 변수로 불러올 수 있다.
path
: secret 경로envVar
: 젠킨스에서 사용할 환경 변수 키vaultKey
Vault에서 불러올 변수 키
# Jenkinsfile
pipeline {
agent any
environment {
VAULT_ADDR = 'http://192.168.0.2: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}"
}
}
}
}
}
}
파이프라인 실행 결과
Vault에서 시크릿을 잘 불러오면, Jenkins 로그에서는 다음과 같이 마스킹된 형태로 출력된다.
파이프라인 작성 시, 유의사항
KV Version 차이 주의
- KV Version1은 경로에 data을 넣고 Version2는 경로에 data을 넣지 않는다! - 참고링크
- Version 1 :
secret/data/sampleapp/config
- Version 2 :
secret/sampleapp/config
- Version 1 :
sh 블록 vs script 블록
상황 | 사용 방식 | |
---|---|---|
sh 블록 | $USERNAME, $PASSWORD | |
script 블록 | ${env.USERNAME}, ${env.PASSWORD} |
ArgoCD에서 Vault 연동하기
앞선 Jenkins 연동에 이어, 이번에는 ArgoCD와 Vault를 연동해서 CD 파이프라인에서도 시크릿 값을 안전하게 관리해 보자.
ArgoCD는 GitOps 기반으로 동작하기 때문에, 별도 보안 처리가 없다면 매니페스트 파일에 민감 정보가 Git에 그대로 노출되는 문제가 생길 수 있다. 그래서 필요한 게 바로 ArgoCD Vault Plugin(AVP)이다.
ArgoCD Vault Plugin(AVP)이란?
Helm
이나 Kustomize
를 사용할 때, 템플릿에 정의된 변수 값을 Vault에서 동적으로 주입해 주는 플러그인이다.
즉, 민감한 값은 Git에 저장하지 않고 Vault에서 불러오는 구조로, GitOps 방식에서도 보안을 강화할 수 있다.
ArgoCD Vault Plugin 설치
AVP는 Helm 설치 시 value로 함께 설정할 수 있지만, 이 예제에서는 수동 방식으로 먼저 실습한다.
argocd 네임스페이스에 AppRole 기반 시크릿 생성
먼저, ArgoCD에서 Vault에 접근하려면 Vault 인증 정보를 먼저 등록해야 한다. 이 예제에서는 AppRole 방식을 사용한다.
# argocd 네임스페이스에 AppRole 기반 시크릿 생성
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: b22a9030-dba3-7827-64ce-11609912f59d # Role_ID
AVP_SECRET_ID: 24ff9b31-ebcc-e007-867e-42dc102e5b90 # Secret_ID
EOF
kusomize 실행하여, AVP를 배포한다.
git clone https://github.com/hyungwook0221/argocd-vault-plugin.git
cd argocd-vault-plugin/manifests/cmp-sidecar
# 생성될 메니페스트 파일에 대한 확인
kubectl kustomize .
# -k 옵션으로 kusomize 실행
kubectl apply -n argocd -k .
AVP 확인repo-server
의 컨테이너 1 → 4
변경되어 AVP 관련 컨테이너들이 배포된다.
ArgoCD Application 설정 (Helm + AVP 연동)
ArgoCD Application 리소스 설정(Helm)
GitHub에 저장된 Helm Repo을 배포하며, Helm 매니페스트 내에 변수로 치환된 값(username/password)을 CD 단계에서 Vault 통해서 읽고 렌더링 하여 배포
plugin.name
에 명시된 이름은argocd-repo-server
컨테이너에 등록된 플러그인 이름과 일치해야 한다.HELM_ARGS
는 Helm 실행 시 필요한 사용자 정의 옵션이다
kubectl apply -n argocd -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: demo
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
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
n
ew-values.yaml 파일을 확인하면 다음과 같이 정브르 노출하지 않고, 명시되어 있다. Vault 통해서 시크릿을 읽고 렌더링 하여 배포한다.
실제 배포된 디플로이먼트의 값을 보면 잘 변환되어 들어감을 확인할 수 있다.
SpringBoot에 Vault 연동 하기(작성중)
SpringBoot에서도 Vault를 활용할 수 있음, 키관리 이외에도 Vault Api를 활용하여 다양한 부분에서 사용 가능(동적 런타임 환경변수)
또한 컨피그서버로 SpringCloud Config 사용 시, Backend로 Vault를 사용하여 더욱더 효율적인 사용이 가능
(개발자 추천) Spring Cloud Vault 데모
https://developer.hashicorp.com/vault/tutorials/encryption-as-a-service/eaas-spring-demo
git clone https://github.com/hashicorp-education/learn-vault-spring-cloud
Vault Secret Operator(VSO)
Vault Secret Operator(VSO)는 Kubernetes 환경에서 Vault와 통합하여 시크릿을 자동으로 동기화하는 컨트롤러다. 일반적으로 앱이 Vault API를 직접 호출하거나, Vault Agent 사이드카를 통해 시크릿을 주입받는 방식이 있다면, VSO는 시크릿을 Kubernetes 네이티브 오브젝트(Secret)로 변환해 주는 역할을 한다.
즉, Vault → Kubernetes Secret 변환을 자동으로 수행해 주므로, 애플리케이션은 기존 방식대로 Kubernetes Secret을 참조하기만 하면 된다.
운영팀이 개발 역량이 부족하더라도, VSO를 통해 시크릿 관리가 용이해짐
VSO 실습 환경 구성
VSO Helm Chart 배포를 위한 Values 설정vault-operator-values.yaml
구성은 다음과 같다
defaultVaultConnection:
enabled: true
address: "http://vault.vault.svc.cluster.local:8200" # Vault ClusterIP 주소
skipTLSVerify: false # TLS 비활성화 여부
controller:
manager:
clientCache:
persistenceModel: direct-encrypted # 직접 암호화 방식 사용
storageEncryption:
enabled: true
mount: k8s-auth-mount # Vault Transit 사용 시 mount 경로
keyName: vso-client-cache # Transit 키 이름
transitMount: demo-transit # Transit Secret Engine Mount
kubernetes:
role: auth-role-operator # Vault에 등록된 Kubernetes Role
serviceAccount: vault-secrets-operator-controller-manager
tokenAudiences: ["vault"] # JWT Audience
VSO 배포 및 CRD 확인
# 배포
helm install vault-secrets-operator hashicorp/vault-secrets-operator \
-n vault-secrets-operator-system \
--create-namespace \
--values vault-operator-values.yaml
# 확인
kubectl get pod -n vault-secrets-operator-system
NAME READY STATUS RESTARTS AGE
vault-secrets-operator-controller-manager-6c7fd7747d-fq8xg 2/2 Running 0 23h
# CRD
kubectl get crd | grep vault
hcpvaultsecretsapps.secrets.hashicorp.com 2025-04-09T12:20:38Z
vaultauthglobals.secrets.hashicorp.com 2025-04-09T12:20:38Z
vaultauths.secrets.hashicorp.com 2025-04-09T12:20:38Z
vaultconnections.secrets.hashicorp.com 2025-04-09T12:20:38Z
vaultdynamicsecrets.secrets.hashicorp.com 2025-04-09T12:20:38Z
vaultpkisecrets.secrets.hashicorp.com 2025-04-09T12:20:38Z
vaultstaticsecrets.secrets.hashicorp.com 2025-04-09T12:20:38Z
Dynamic Secret 실습 : PostgreSQL 사용자 계정 자동 갱신
Vault의 Dynamic Secret 기능은 동적으로 변경되는 Vault의 Secrets를 K8s Secrets에 동기화하는 기능이다.
이번 실습에서는 Vault의 Dynamic Secret Engine을 활용해, PostgreSQL 사용자 계정을 Vault에서 동적으로 생성하고, VSO를 통해 주기적으로 갱신된 정보를 Kubernetes Secret에 반영하여 Spring 애플리케이션에서 참조하도록 구성해 본다.
(데이터베이스 접근을 테스트하는 샘플 애플리케이션(Springboot))
PostgreSQL 설치 (Bitnami Helm Chart)
auth.postgresPassword: 초기 postgres 계정의 비밀번호를 지정한다. 이후 Vault가 이 계정을 통해 DB에 접속하여 사용자 계정을 동적으로 생성하게 된다
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
# psql 로그인 확인
kubectl exec -it -n postgres postgres-postgresql-0 -- sh -c 'PGPASSWORD=secret-pass psql -U postgres -h localhost'
kubectl exec -it -n postgres postgres-postgresql-0 -- sh -c "PGPASSWORD=secret-pass psql -U postgres -h localhost -c '\l'"
Kubernetes Auth Method 설정
Vault가 K8s API 서버에 인증 요청을 보낼 수 있게 하기 위한 구체적인 인증 설정
Vault가 K8s의 ServiceAccount를 사용해 Pod 단위 인증을 할 수 있도록 설정하는 핵심 부분이다.
- kubernetes_host
: API 서버 주소
- kubernetes_ca_cert
: API 서버의 CA 인증서 (Pod 내 ServiceAccount에서 자동 주입됨)
- token_reviewer_jwt
: TokenReviewer 권한이 있는 토큰으로 Vault가 요청자의 토큰을 검증할 수 있도록 설정
# Kubernetes 인증 메서드 활성화
vault auth enable -path k8s-auth-mount kubernetes
vault auth list
Path Type Accessor Description Version
---- ---- -------- ----------- -------
approle/ approle auth_approle_eb986350 n/a n/a
k8s-auth-mount/ kubernetes auth_kubernetes_ebf4f28c n/a n/a
kubernetes/ kubernetes auth_kubernetes_bbaa6327 n/a n/a
token/ token auth_token_16027d39 token based credentials n/a
# 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 read auth/k8s-auth-mount/config
Vault Kubernetes Auth Role 생성
- 이 Role은 지정된 ServiceAccount만 Vault 접근 허용
- 이 Role을 기반으로 동적으로 PostgreSQL 계정 생성을 할 수 있음
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 Database Secret Engine 설정
- Vault에 Database Secret Engine을 등록
path=demo-db
는 이 시크릿 엔진의 접근 경로
# demo-db라는 경로로 Database Secret Engine을 활성화
vault secrets enable -path=demo-db database
- PostgreSQL 접속 정보를 Vault에 알려주는 명령
- Vault는 해당 접속 정보를 이용해 사용자 계정을 생성하게 됨
# 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"
- 실제 사용자 계정을 만드는 SQL 문을 명시
{{name}}
,{{password}}
,{{expiration}}
은 Vault가 채워줌- TTL이 짧게 설정되어 있어 자격 증명은 자동 폐기됨
# DB 사용자 동적 생성 Role 등록
# 해당 Role 사용 시 Vault가 동적으로 사용자 계정과 비밀번호를 생성 가능
# TTL은 생성된 자격증명의 유효 시간 (30초~10분)
## creation_statements=... : Vault가 계정을 자동 생성할 때 실행할 SQL 문. {{name}}, {{password}}, {{expiration}}은 Vault가 자동으로 채워줌.
## revocation_statements=... : Vault가 동적 사용자 계정을 폐기(revoke)할 때 실행할 SQL 문. 권한을 모두 회수.
## default_ttl="1m" / max_ttl="1m" : 기본 1분만 유효합니다. 갱신을 안 하면 1분 후 자동 만료 (계정도 자동 삭제). 최대 TTL도 1분이므로 연장도 최대 1분까지만.
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 정책 작성
- 지정된
path
의 시크릿을 읽을 수 있는 권한을 가진 정책 - 위에서 만든
auth-role
에 이 정책을 바인딩함
# 정책 설정: 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
Transit Secret Engine 설정 + VSO 연동 Role 구성
transit
엔진은 시크릿을 저장하지 않고 암복호화 기능만 수행- VSO는 이 엔진을 사용하여 자체 캐시 데이터를 암호화한다
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 -detailed
# 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
VSO가 Vault에 접근하기 위한 전용 Role 및 권한 정책
# Vault Secrets Operator가 사용하는 ServiceAccount에 위 정책을 바인딩
# 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
샘플 애플리케이션 YAML 작성 및 배포
demo-ns 네임스페이스 생성
kubectl create ns demo-ns
mkdir vso-dynamic
cd vso-dynamic
vault-auth-dynamic.yaml
---
# 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
app-secret.yaml
Spring App에서 PostgreSQL에 접속할 때 사용할 해당 시크릿에 username/password을 동적으로 생성
---
# app-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: vso-db-demo
namespace: demo-ns
vault-dynamic-secret.yaml
- Vault에서 동적으로 PostgreSQL 접속정보 생성하고 K8s Secret에 저장
- 생성된 Secret(vso-db-demo)은 앱에서 환경 변수(env)로 사용
- 애플리케이션에서 Dynamic Reloading을 지원하지 않을 경우
rolloutRestartTargets을
사용하여 애플리케이션을 재배포하여 새로 업데이트된 시크릿을 사용하도록 할 수 있음 refreshAfter
설정을 통해 VSO가 소스 시크릿 데이터를 동기화하는 주기 설정가능 - Docs → Event Notification 기능을 통해 변경이 있을 경우 즉시 반영할 수도 있음 - Docs
---
# 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
app-spring-deploy.yaml
DB 접속 테스트를 위한 Spring App →
---
# 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: 30003
selector:
app: vaultdemo
type: NodePort
애플리케이션 배포 : kubectl apply -f .
동작확인(UI) : postgres-postgresql.postgres.svc
, db는 postgres
- localhost:30003 접속 후 K8s Secrets에 생성된 username, password 값으로 연결 테스트
동작확인(CLI): 생성된 계정 정보 확인
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-k8s-auth-dev-post-03VpsmncLlPCYFFzs8nS-1744259323 | Password valid until 2025-04-10 04:29:48+00
v-k8s-auth-dev-post-04yhltbKChlXRACTISxq-1744247061 | Password valid until 2025-04-10 01:05:26+00
v-k8s-auth-dev-post-0MhpqR4Ve4dj2SJwEC1h-1744269821 | Password valid until 2025-04-10 07:24:46+00
v-k8s-auth-dev-post-0Qm0Nz3yBeAKZE2E9s7T-1744252998 | Password valid until 2025-04-10 02:44:23+00
v-k8s-auth-dev-post-0RqEzgzqksKyzwEHhhQI-1744248273 | Password valid until 2025-04-10 01:25:38+00
v-k8s-auth-dev-post-0b7o1fqXsDJY95eJnDJx-1744284279 | Password valid until 2025-04-10 11:25:44+00
v-k8s-auth-dev-post-0i9826epaGdGd2Vzw2HN-1744268874 | Password valid until 2025-04-10 07:08:59+00
v-k8s-auth-dev-post-0l8Lw1Pty8X4mfvbIc1d-1744246932 | Password valid until 2025-04-10 01:03:17+00
v-k8s-auth-dev-post-0oZm5uWn3mC5MoZ9QOuq-1744281463 | Password valid until 2025-04-10 10:38:48+00
v-k8s-auth-dev-post-0zdabtnrXfxmDt5fHHK7-1744250806 | Password valid until 2025-04-10 02:07:51+00
v-k8s-auth-dev-post-141BomvTZ81ZkGPDHbm2-1744247808 | Password valid until 2025-04-10 01:17:53+00
v-k8s-auth-dev-post-14epljd5473NZRIiSR4Q-1744259976 | Password valid until 2025-04-10 04:40:41+00
v-k8s-auth-dev-post-1AJWec4UnmQz08RVciNu-1744274396 | Password valid until 2025-04-10 08:41:01+00
v-k8s-auth-dev-post-1ApxN1VdngTVgaL5AyQh-1744250320 | Password valid until 2025-04-10 01:59:45+00
v-k8s-auth-dev-post-1BkZ2C6jzINbaqbQt3WM-1744250408 | Password valid until 2025-04-10 02:01:13+00
v-k8s-auth-dev-post-1ET0cyjcHvlGtODFaRSb-1744248231 | Password valid until 2025-04-10 01:24:56+00
v-k8s-auth-dev-post-1QBIU61qUAGI2RgvbtdK-1744262707 | Password valid until 2025-04-10 05:26:12+00
v-k8s-auth-dev-post-1c8CW4iNp2cy7iTLxjuu-1744271727 | Password valid until 2025-04-10 07:56:32+00
v-k8s-auth-dev-post-1mZhrXptkzIVbyiA9tDM-1744269348 | Password valid until 2025-04-10 07:16:53+00
v-k8s-auth-dev-post-1qydCqRJp7SstjDKcyds-1744259717 | Password valid until 2025-04-10 04:36:22+00
while true; do kubectl get pod -n demo-ns ; echo; kubectl view-secret -n demo-ns vso-db-demo --all; date; sleep 30 ; echo ; done
NAME READY STATUS RESTARTS AGE
vaultdemo-55f977d777-wwlr8 1/1 Running 0 39s
_raw={"password":"2YQc2pqDca0WN-C6miEE","username":"v-k8s-auth-dev-post-6lkizjMmcCnDFICFxCHu-1744285278"}
password=2YQc2pqDca0WN-C6miEE
username=v-k8s-auth-dev-post-6lkizjMmcCnDFICFxCHu-1744285278
2025년 4월 10일 목요일 20시 41분 57초 KST
NAME READY STATUS RESTARTS AGE
vaultdemo-5d4fd79475-vksx6 1/1 Running 0 27s
_raw={"password":"AwKYWJ3tQ-Q9POWXy-iQ","username":"v-k8s-auth-dev-post-fnJ20MDo8dzcdXuUAUvM-1744285320"}
password=AwKYWJ3tQ-Q9POWXy-iQ
username=v-k8s-auth-dev-post-fnJ20MDo8dzcdXuUAUvM-1744285320
2025년 4월 10일 목요일 20시 42분 27초 KST
PKI Secrets 실습 : Nginx TLS 인증서 자동 갱신
Vault는 PKI(공개키 기반 구조)를 Secret Engine 형태로 지원하여 자체적으로 루트 또는 중간 CA 역할을 할 수 있다
실습 시나리오는 다음과 같다.
- Vault에서 TLS 인증서를 자동 발급
- Vault Secrets Operator(VSO)를 통해 Kubernetes Secret으로 동기화
- Nginx Deployment에 주입하여 자동 갱신까지 확인
Vault PKI 시크릿엔진 사용을 위한 설정
hackjap-pki
: PKI Secret Engine 마운트 경로common_name
: 발급할 Root CA의 CN(Common Name)ttl
: 인증서 유효 기간 (100년)
# 생성
vault secrets enable -path=hackjap-pki pki
vault write pki/root/generate/internal \
common_name="ault-pki.hackjap.com" \
ttl=87600h
# 확인
vault read -field=certificate pki-test/cert/ca | openssl x509 -noout -subject
CA 및 CRL Distribution 설정
Vault는 PKI Secret Engine을 통해 자체적으로 루트 또는 중간 인증 기관(CA) 역할하는데, 클라이언트가 인증서를 검증 시, 해당 URL 정보를 통해 인증을 수행
TODO - 해당 정보가 인증서에 명시되어 있는지 확인필요
- Vault가 발급하는 인증서에 포함될 CA/CRL 메타데이터
vault write pki-test/config/urls \ issuing_certificates="http://vault.vault.svc:8200/v1/pki/ca" \ crl_distribution_points="http://vault.vault.svc:8200/v1/pki/crl"
인증서 발급 Role 설정
allowed_domains
: 발급 가능한 도메인 리스트- SAN이 아니라, Vault의 사전 도메인 체크 정책
allow_any_name=true
: SAN, CN 검증 무시하고 자유롭게 발급 허용
vault write pki-test/roles/cert-role \
allowed_domains="vault-pki.hackjap.com,hashicorp-app.svc.cluster.local" \
allow_subdomains=true \
allow_bare_domains=true \
allow_any_name=true \
enforce_hostnames=false \
max_ttl="24h"
등록 확인
Policy 및 AppRole 구성pki-policy
: 인증서 발급 권한을 부여하는 정책
vault policy write pki-policy - <<EOF
path "pki-test/issue/cert-role" {
capabilities = ["update", "read", "list"]
}
EOF
cert-role
: 인증서 발급을 위한 Role
vault write auth/approle/role/cert-role \
token_policies="pki-policy" \
token_ttl="30m" \
token_max_ttl="1h"
Kubernetes 연동 구성 (VSO)webapp
네임스페이스를 생성하여, VSO의 CR(Custom Resource)를 배포합니다.
kubectl create ns webapp
AppRole 인증 정보 전달
kubectl create secret generic vso-cert-role \
-n webapp \
--from-file=role_id=role_id.txt \
--from-file=id=secret_id.txt
VaultAuth CR 생성
kubectl create -f - <<EOF
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: pki-auth
namespace: webapp
spec:
method: appRole
mount: approle
appRole:
roleId: 0ed6d0b4-7b56-fdab-df41-4df2855ef14c
secretRef: vso-cert-role
EOF
VaultPKISecret CR 생성
destination.name
: Kubernetes Secret으로 저장될 이름rolloutRestartTargets
: 인증서 변경 시 자동 재시작 대상 리소스ttl
: 인증서 유효 기간: 인증서가 변경되는 유효 기간
# 1. vault agent 역할. 사용자를 대행하여 vault에 주기적 요청을 통해 인증서를 갱신
# 2. 인증서를 사용하는 devployment에 대해 롤 아웃 수행하여 바뀐 인증서 정보 반영
kubectl apply -f - <<EOF
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultPKISecret
metadata:
name: hashicorp-tls
namespace: webapp
spec:
vaultAuthRef: pki-auth
mount: pki-test
role: cert-role
commonName: vault-pki.hackjap.com
ttl: 30m
destination:
create: true
name: vso-pki-cert
rolloutRestartTargets:
- kind: Deployment
name: nginx-vault-ssl
EOF
등록한 도메인 vault-pki.hackjap.com
으로 브라우저에서 접근하여 인증서 정보를 확인한다.
Vault 웹에서도 같은 일련번호의 Certificate 인증서 정보를 확인할 수 있다.
해당 CA 인증서를 받아, Mac의 경우, Keychain 등록한다.(유효기간 일치 확인)
브라우저 새 창을 통해 다시 접속하였더니, 인증서가 유효함을 확인할 수 있습니다.
유효기간 검증: 유효시간이 지나고, 다시 접속해 보면, 인증서가 유효하지 않음을 확인할 수 있다.
'CICD' 카테고리의 다른 글
GoCD 설치 및 기본 사용법 정리 (Feat. Kubernetes) (0) | 2025.04.04 |
---|---|
README.md만 수정했는데 ArgoCD가 파드를 재배포해요… 이거 진짜에요?(Diffing Customization) (0) | 2025.03.29 |
GitLab CI/CD 기본 사용 방법 및 CI/CD 워크플로우 구성 (0) | 2024.12.21 |
ArgoCD Autopilot: ArgoCD 및 GitOps 환경 자동 구축하기 (2) | 2024.12.19 |
Github Actions 기본 사용 방법 및 CI/CD 워크플로우 구성하기 (2) | 2024.12.14 |