실습 환경 구성(WSL2 + Docker + kind)
WSL + Ubuntu 24.04
Powershell 관리자 권한으로 실행
# DISM(배포 이미지 서비스 및 관리) 명령어로 Microsoft-Windows-Subsystem-Linux 기능을 활성화
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
# DISM 명령어로 VirtualMachinePlatform 기능을 활성화
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
# wsl 설치
wsl --install
# 기본값 WSL 2 버전 설정
wsl --set-default-version 2
# wsl 업데이트
wsl --update
윈도우 OS 재부팅
Powershell 관리자 권한으로 실행
# 설치 가능한 배포판 확인
wsl --list --online
# Ubuntu 배포판 설치
wsl --install Ubuntu-24.04
...
Enter new UNIX username: <각자 Ubuntu 사용 계정>
New password: <해당 계정 암호>
Retype new password: <해당 계정 암호>
passwd: password updated successfully
Installation successful!
To run a command as administrator (user "root"), use "sudo <command>".
---------------------------------------
# 기본 정보 확인
hostnamectl
whoami
id
pwd
# apt 업데이트
sudo apt update
sudo apt install jq htop curl wget ca-certificates net-tools -y
ifconfig eth0
ping -c 1 8.8.8.8
# 빠져나오기
$ exit
---------------------------------------
# 설치된 배포판 확인
wsl -l -v
# Ubuntu 재진입
wsl
WSL2 에 Docker 설치
# WSL2 에 Docker 설치 : 아래 스크립트 실행 후 20초 대기하면 스크립트 실행 됨
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh ./get-docker.sh
...
# 설치 확인
docker info
docker ps
sudo systemctl status docker
cat /etc/group | grep docker
VSCODE 설치, WSL Extension 설치 (Download , WSL-VSCODE)
WSL2 에 kind 및 관리 툴 설치
# 기본 사용자 디렉터리 이동
cd $PWD
pwd
#
sudo systemctl stop apparmor && sudo systemctl disable apparmor
#
sudo apt update && sudo apt-get install bridge-utils net-tools jq tree unzip kubectx kubecolor -y
# Install Kind
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.27.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
kind --version
# Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv ./kubectl /usr/bin
sudo kubectl version --client=true
# Install Helm
curl -s https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
helm version
# Source the completion
source <(kubectl completion bash)
echo 'source <(kubectl completion bash)' >> ~/.bashrc
# Alias kubectl to k
echo 'alias k=kubectl' >> ~/.bashrc
echo 'complete -o default -F __start_kubectl k' >> ~/.bashrc
# Install Kubeps & Setting PS1
git clone https://github.com/jonmosco/kube-ps1.git
echo -e "source $PWD/kube-ps1/kube-ps1.sh" >> ~/.bashrc
cat <<"EOT" >> ~/.bashrc
KUBE_PS1_SYMBOL_ENABLE=true
function get_cluster_short() {
echo "$1" | cut -d . -f1
}
KUBE_PS1_CLUSTER_FUNCTION=get_cluster_short
KUBE_PS1_SUFFIX=') '
PS1='$(kube_ps1)'$PS1
EOT
# .bashrc 적용
source ~/.bashrc
kind 기본 사용 - 클러스터 배포 및 확인
# 클러스터 배포 전 확인
docker ps
# Create a cluster with kind
kind create cluster
# 클러스터 배포 확인
kind get clusters
kind get nodes
kubectl cluster-info
# 노드 정보 확인
kubectl get node -o wide
# 파드 정보 확인
kubectl get pod -A
kubectl get componentstatuses
# 컨트롤플레인 (컨테이너) 노드 1대가 실행
docker ps
docker images
# kube config 파일 확인
cat ~/.kube/config
혹은
cat $KUBECONFIG # KUBECONFIG 변수 지정 사용 시
# nginx 파드 배포 및 확인 : 컨트롤플레인 노드인데 파드가 배포 될까요?
kubectl run nginx --image=nginx:alpine
kubectl get pod -owide
# 노드에 Taints 정보 확인
kubectl describe node | grep Taints
Taints: <none>
# 클러스터 삭제
kind delete cluster
# kube config 삭제 확인
cat ~/.kube/config
혹은
cat $KUBECONFIG # KUBECONFIG 변수 지정 사용 시
kind 로 k8s 배포
- 기본 정보 확인
# 클러스터 배포 전 확인
docker ps
mkdir cicd-labs
cd ~/cicd-labs
# WSL2 Ubuntu eth0 IP를 지정
ifconfig eth0
MyIP=<각자 자신의 WSL2 Ubuntu eth0 IP>
# 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
- 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
kubens default
# kind 는 별도 도커 네트워크 생성 후 사용 : 기본값 172.18.0.0/16
docker network ls
docker inspect kind | jq
# k8s api 주소 확인 : 어떻게 로컬에서 접속이 되는 걸까?
kubectl cluster-info
# 노드 정보 확인 : CRI 는 containerd 사용
kubectl get node -o wide
# 파드 정보 확인 : CNI 는 kindnet 사용
kubectl get pod -A -o wide
# 네임스페이스 확인 >> 도커 컨테이너에서 배운 네임스페이스와 다릅니다!
kubectl get namespaces
# 컨트롤플레인/워커 노드(컨테이너) 확인 : 도커 컨테이너 이름은 myk8s-control-plane , myk8s-worker/worker-2 임을 확인
docker ps
docker images
# 디버그용 내용 출력에 ~/.kube/config 권한 인증 로드
kubectl get pod -v6
# kube config 파일 확인 : "server: https://<자신의 IP>:35413" 부분에 접속 주소 잘 확인해두자!
cat ~/.kube/config
ls -l ~/.kube/config
실습 환경 구성 (Jenkins)
- Jenkins 컨테이너 기동
# 작업 디렉토리 생성 후 이동
mkdir cicd-labs
cd cicd-labs
# cicd-labs 작업 디렉토리 IDE(VSCODE 등)로 열어두기
# kind 설치를 먼저 진행하여 docker network(kind) 생성 후 아래 Jenkins,gogs 생성 할 것
# docker network 확인 : kind 를 사용
docker network ls
...
7e8925d46acb kind bridge local
...
#
cat <<EOT > docker-compose.yaml
services:
jenkins:
container_name: jenkins
image: jenkins/jenkins
restart: unless-stopped
networks:
- kind
ports:
- "8080:8080"
- "50000:50000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- jenkins_home:/var/jenkins_home
volumes:
jenkins_home:
networks:
kind:
external: true
EOT
# 배포
docker compose up -d
docker compose ps
docker inspect kind
# 기본 정보 확인
for i in jenkins ; do echo ">> container : $i <<"; docker compose exec $i sh -c "whoami && pwd"; echo; done
# 도커를 이용하여 각 컨테이너로 접속
docker compose exec jenkins bash
exit
- Jenkins 컨테이너 초기 설정
# Jenkins 초기 암호 확인
docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword
# Jenkins 웹 접속 주소 확인 : 계정 / 암호 입력 >> admin / qwe123
웹 브라우저에서 http://127.0.0.1:8080 접속 # Windows
# (참고) 로그 확인 : 플러그인 설치 과정 확인
docker compose logs jenkins -f
실습 환경 구성(ArgoCD)
- ArgoCD 설치
# 네임스페이스 생성 및 파라미터 파일 작성
cd cicd-labs
kubectl create ns argocd
cat <<EOF > argocd-values.yaml
dex:
enabled: false
server:
service:
type: NodePort
nodePortHttps: 30002
extraArgs:
- --insecure # HTTPS 대신 HTTP 사용
EOF
# 설치
helm repo add argo https://argoproj.github.io/argo-helm
helm install argocd argo/argo-cd --version 7.8.13 -f argocd-values.yaml --namespace argocd
# 확인
kubectl get pod,svc,ep,secret,cm -n argocd
kubectl get crd | grep argo
kubectl get appproject -n argocd -o yaml
# configmap
kubectl get cm -n argocd argocd-cm -o yaml
kubectl get cm -n argocd argocd-rbac-cm -o yaml
...
data:
policy.csv: ""
policy.default: ""
policy.matchMode: glob
scopes: '[groups]'
# 최초 접속 암호 확인
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d ;echo
XxJMMJUv8MHZa-kk
# Argo CD 웹 접속 주소 확인 : 초기 암호 입력 (admin 계정)
open "http://127.0.0.1:30002" # macOS
# Windows OS경우 직접 웹 브라우저에서 http://127.0.0.1:30002 접속
- Argo CD 웹 접속 확인
- User info → UPDATE PASSWORD 로 admin 계정 암호 변경 (qwe12345)
이번 실습에서 ArgoCD Vault Plugin 추가 진행 예정
Vault 개요
Vault란?
HashiCorp Vault는 신원 기반(identity-based)의 시크릿 및 암호화 관리 시스템입니다. 이 시스템은 인증(authentication) 및 인가(authorization) 방법을 통해 암호화 서비스를 제공하여 비밀에 대한 안전하고 감사 가능하며 제한된 접근을 보장합니다.
시크릿(Secret)이란 접근을 철저히 통제하고자 하는 모든 것을 의미하며, 예를 들어 토큰, API 키, 비밀번호, 암호화 키 또는 인증서 등이 이에 해당합니다. Vault는 모든 시크릿에 대해 통합된 인터페이스를 제공하면서, 엄격한 접근 제어와 상세한 감사 로그 기록 기능을 제공합니다.
외부 서비스용 API 키, 서비스 지향 아키텍처 간 통신을 위한 자격 증명 등은 플랫폼에 따라 누가 어떤 비밀에 접근했는지를 파악하기 어려울 수 있습니다. 여기에 키 롤링(교체), 안전한 저장, 상세한 감사 로그까지 추가하려면 별도의 커스텀 솔루션 없이는 거의 불가능합니다. Vault는 바로 이 지점에서 해결책을 제공합니다.
Vault는 클라이언트(사용자, 기계, 애플리케이션 등)를 검증하고 인가한 후에만 비밀이나 저장된 민감한 데이터에 접근할 수 있도록 합니다.
- 대표적인 시크릿의 종류
- 비밀번호
- Cloud Credentials : AWS, GCP, Azure, NCP
- Database Credentials : MySQL,
- SSH Key
- Token, API Key : GitHub, Telegram, Slack, OpenAI, Claude
- 인증서(PKI, TLS 등)
Vault 동작방식
Vault는 주로 토큰(Token)을 기반으로 작동하며, 이 토큰은 클라이언트의 정책(Policy)과 연결되어 있습니다. 각 정책은 경로(path) 기반으로 설정되며, 정책 규칙은 클라이언트가 해당 경로에서 수행할 수 있는 작업과 접근 가능성을 제한합니다.
Vault에서는 토큰을 수동으로 생성해 클라이언트에 할당할 수도 있고, 클라이언트가 로그인하여 토큰을 직접 획득할 수도 있습니다.
- 아래 그림은 Vault의 핵심 워크플로우를 보여줍니다.
Vault의 핵심 워크플로우는 다음 네 단계로 구성됩니다:
- 인증 (Authenticate): Vault에서 인증은 클라이언트가 Vault에 자신이 누구인지 증명할 수 있는 정보를 제공하는 과정입니다. 클라이언트가 인증 메서드를 통해 인증되면, 토큰이 생성되고 정책과 연결됩니다.
- 검증 (Validation): Vault는 Github, LDAP, AppRole 등과 같은 신뢰할 수 있는 외부 소스를 통해 클라이언트를 검증합니다.
- 인가 (Authorize): 클라이언트는 Vault의 보안 정책과 비교됩니다. 이 정책은 Vault 토큰을 사용하여 클라이언트가 접근할 수 있는 API 엔드포인트를 정의하는 규칙의 집합입니다. 정책은 Vault 내 특정 경로나 작업에 대한 접근을 허용하거나 거부하는 선언적 방식으로 권한을 제어합니다.
- 접근 (Access): Vault는 클라이언트의 신원에 연관된 정책을 기반으로 토큰을 발급하여 비밀, 키, 암호화 기능 등에 대한 접근을 허용합니다. 클라이언트는 이후 작업에서 해당 Vault 토큰을 사용할 수 있습니다.
Vault의 필요성
오늘날 대부분의 기업은 자격 증명이 조직 전반에 걸쳐 무분별하게 퍼져 있습니다.
비밀번호, API 키, 자격 증명 등이 일반 텍스트로 앱 소스 코드, 설정 파일, 기타 여러 위치에 저장되어 있습니다.
자격 증명이 이처럼 여기저기 흩어져 있으면 누가 무엇에 접근하고 권한이 있는지를 명확히 파악하기 어렵고, 그로 인해 큰 부담이 따릅니다.
일반 텍스트로 자격 증명을 저장하면 내부 공격자든 외부 공격자든 악의적인 공격 가능성이 크게 증가합니다.
Vault는 이러한 문제를 해결하기 위해 설계되었습니다.
Vault는 이러한 모든 자격 증명을 한 곳에 중앙 집중화하여 정의함으로써, 자격 증명의 불필요한 노출을 줄입니다.
하지만 Vault는 여기서 멈추지 않고, 사용자, 애플리케이션, 시스템이 인증 및 명시적으로 인가된 후에만 리소스에 접근할 수 있도록 보장하며, 클라이언트의 모든 작업 기록을 추적하고 저장하는 감사 로그 기능도 제공합니다.
Vault의 주요 기능은 다음과 같습니다:
1. 안전한 비밀 저장 (Secure Secret Storage): → Static 시크릿
Vault는 임의의 key/value 형식의 시크릿을 저장할 수 있으며, 이 시크릿은 영구 저장소에 기록되기 전에 암호화됩니다. 따라서 저장소에 직접 접근하더라도 비밀을 열람할 수 없습니다. Vault는 Disk, Consul 등 다양한 저장소를 지원합니다.
2. 동적 비밀 (Dynamic Secrets):
Vault는 AWS나 SQL 데이터베이스와 같은 일부 시스템에 대해 요청 시 비밀을 동적으로 생성할 수 있습니다. 예를 들어, 애플리케이션이 S3 버킷에 접근해야 할 때 Vault에 자격 증명을 요청하면, Vault는 해당 권한을 가진 AWS 키쌍을 생성해줍니다. 이 동적 시크릿은 일정 시간이 지나면 자동으로 폐기됩니다.
3. 데이터 암호화 (Data Encryption):
Vault는 데이터를 저장하지 않고 암호화 및 복호화를 수행할 수 있습니다. 이를 통해 보안 팀은 암호화 매개변수를 정의하고, 개발자는 암호화된 데이터를 SQL 데이터베이스 등 외부 저장소에 안전하게 저장할 수 있습니다.
4. 임대 및 갱신 (Leasing and Renewal):
Vault에 저장된 모든 시크릿은 임대 기간(lease)이 설정되어 있으며, 이 기간이 끝나면 해당 비밀은 자동으로 폐기됩니다. 클라이언트는 내장된 갱신 API를 통해 임대를 연장할 수 있습니다.
5. 폐기 (Revocation):
Vault는 비밀 폐기를 기본적으로 지원합니다. 단일 비밀뿐만 아니라 특정 사용자에 의해 읽힌 모든 비밀, 또는 특정 유형의 모든 비밀 등 비밀의 계층 구조 전체를 폐기할 수 있습니다. 이 기능은 키 롤링이나 침입 발생 시 시스템을 신속하게 차단하는 데 유용합니다.
Vault 기본 구조 이해
Kubernetes에 Vault 설치
- 실습용 K8s 환경(KinD)에 Vault를 Helm으로 설치
- 설치된 Vault 서버에 접속하고 UI/CLI로 기본 기능을 확인
- Vault의 동작 방식과 구조의 이해
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
- Helm Chart 설정 Values 설정 및 배포
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
disable_mlock = true
cluster_name = "vault-local"
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_disable = 1
}
storage "raft" { # Raft 구성 권장
path = "/vault/data"
node_id = "vault-dev-node-1"
}
service:
enabled: true
type: NodePort
port: 8200
targetPort: 8200
nodePort: 30000 # Kind에서 열어둔 포트 중 하나 사용
injector:
enabled: true
ui:
enabled: true
serviceType: "NodePort"
EOF
# 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
Vault 초기화 및 잠금해제
- 상태 확인
# Vault Status 명령으로 Sealed 상태확인
kubectl exec -ti vault-0 -- vault status
- init-unseal.sh 을 사용하여 Vault Unseal 자동화
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
- Root Token 입력 후 Vault UI 화면 - vault-root-token.txt 파일에서 획득
- CLI 설정
# Ubuntu에서 설치
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
# NodePort로 공개한 30000 Port로 설정
export VAULT_ADDR='http://localhost:30000'
# vault 상태확인
vault status
# Root Token으로 로그인
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.egeVgnhMKgnFqof6YLfiPtLa
token_accessor wWpBsO3TqQEybFF0le2YmNGX
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
KV 시크릿 엔진 활성화 및 샘플 구성 → Static Secret
→ Vault KV version 2 엔진을 활성화하고 샘플 데이터를 저장합니다.
→ Version1 : KV 버전관리 불가 / Version2 : KV 버전관리 가능
KV 엔진 활성화 및 샘플 데이터 추가
# KV v2 형태로 엔진 활성화
vault secrets enable -path=secret kv-v2
# 샘플 시크릿 저장
vault kv put secret/sampleapp/config \
username="demo" \
password="p@ssw0rd"
# 입력된 데이터 확인
vault kv get secret/sampleapp/config
Vault UI : [Secrets Engine] 탭에 접속 후 [sampleapp - config] 접속하여 실제 저장된 Key / Value 확인
- username : demo
- password : p@ssw0rd
(참고) 경로 확인하는 명령 가이드
Vault Sidecar 연동 (Vault Agent)
- Vault Agent Sidecar 패턴을 이해하고, 앱(Pod)이 시크릿을 볼륨으로 마운트하여 사용하도록 구성한다.
- Vault Agent Injector 구성을 위해 Helm Chart(https://github.com/hashicorp/vault-helm) 에서 활성화 해야하며, https://github.com/hashicorp/vault-k8s을 활용한다.
공식문서 요약(ChatGPT)
1. Vault가 설치되어 있고, Kubernetes와 통합되어 있어야 합니다
- Vault가 실행 중이어야 하고, Kubernetes 클러스터에 접근 가능해야 합니다.
- Vault는 Kubernetes 인증 방식을 설정하고 있어야 하며, 이를 통해 서비스 어카운트를 기반으로 토큰을 발급받을 수 있습니다.
2. Vault Agent Injector가 클러스터에 배포되어 있어야 합니다
- Injector는 Kubernetes에 배포되는 별도의 구성 요소입니다.
- 일반적으로 Helm Chart를 통해 배포하며, 이 컴포넌트가 있어야 Pod에 Vault Agent가 자동으로 주입됩니다.
3. Kubernetes 인증 방식이 활성화되어야 합니다
- Vault에서 Kubernetes Auth Method를 활성화하고 구성해야 합니다.
- 이 설정을 통해 특정 서비스 어카운트에 Vault 접근 권한을 부여할 수 있습니다.
4. 정책과 역할이 정의되어 있어야 합니다
- Vault에 접근할 수 있도록 적절한 Policy와 Role이 설정되어야 합니다.
- 예를 들어, 특정 서비스 어카운트가 특정 경로의 시크릿에만 접근할 수 있도록 제한할 수 있습니다.
5. 애플리케이션 Pod에 주입할 주석(annotation)을 추가해야 합니다
- Vault Agent Injector는 특정 주석이 있는 Pod에 대해서만 Vault Agent를 주입합니다.
- Vault Agent Injector는 Kubernetes Pod 내부에 Vault Agent를 자동으로 주입해주는 기능입니다. 이를 통해 어플리케이션이 Vault로부터 자동으로 비밀 정보를 받아올 수 있게 됩니다. 하지만 이를 사용하기 전에 몇 가지 사전 준비가 필요합니다.
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "example-role"
Vault Kubernetes Sidecar 아키텍처 및 워크플로우

⎈ A Hands-On Guide to Vault in Kubernetes ⚙️
⇢ Manage k8s Secrets Using HashiCorp Vault: With Practical Examples
medium.com
Vault AppRole 방식 인증 구성
- 인증 구성 및 정책 적용
# 1. AppRole 인증 방식 활성화
vault auth enable approle || echo "AppRole already enabled"
vault auth list
# 2. 정책 생성
vault policy write sampleapp-policy - <<EOF
path "secret/data/sampleapp/*" {
capabilities = ["read"]
}
EOF
# 3. AppRole Role 생성
vault write auth/approle/role/sampleapp-role \
token_policies="sampleapp-policy" \
secret_id_ttl="1h" \
token_ttl="1h" \
token_max_ttl="4h"
# 4. Role ID 및 Secret ID 추출 및 저장
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"
# 5. 파일로 저장
mkdir -p approle-creds
echo "$ROLE_ID" > approle-creds/role_id.txt
echo "$SECRET_ID" > approle-creds/secret_id.txt
# 6. (옵션) Kubernetes Secret으로 저장
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 -
Vault Agent Sidecar 연동
Vault Agent는 vault-agent-config.hcl 설정을 통해 연결할 Vault의 정보와, Template 구성, 렌더링 주기, 참조할 Vault KV 위치정보 등을 정의한다.
1. Vault Agent 설정 파일 작성 및 생성 (vault-agent-config.hcl) - HCL
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
2. 샘플 애플리케이션 + Sidecar 배포(수동방식)
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
3. SVC 생성
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
4. 생성된 컨테이너 확인
# 파드 내에 사이드카 컨테이너 추가되어 2/2 확인
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-config.hcl
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 stern -l app=nginx-vault-demo -c vault-agent-sidecar
# mutating admission
kubectl get mutatingwebhookconfigurations.admissionregistration.k8s.io
5. 실제 배포된 화면 확인
6. Key-Value 값 변경 후 확인
Jenkins + Vault (AppRole) - CI
- Vault KV Store에 저장한 username, password을 Jenkins을 활용해서 획득하는 방안
- CI 파이프라인에서 정적(Static) 시크릿을 외부에 저장하고 관리할 경우 사용할 수 있습니다.
CI/CD 보안 고려사항
CI/CD 안티패턴
Retrieving CI/CD secrets from Vault | HashiCorp Developer
Explore HashiCorp product documentation, tutorials, and examples.
developer.hashicorp.com
최근 CI/CD의 공격사례(CVE-2025-30066) : GitHub Action tj-actions/changed-files 공격
Vault - Jenkins Plugin with AppRole 인증방식 워크플로우
공식문서 요약(ChatGPT)
젠킨스는 Vault에 시크릿으로 분류된 데이터를 필요로 하는 작업(job)을 실행해야 합니다. 젠킨스는 마스터 노드와 워커 노드를 가지고 있으며, 워커 노드는 짧은 시간 동안 실행되는 컨테이너 러너에서 작업을 실행합니다.
- 프로세스는 다음과 같습니다:
- 젠킨스 워커가 Vault에 인증
- Vault는 토큰을 반환
- 워커는 이 토큰을 사용해 작업에 해당하는 역할의 Wrapped SecretID를 요청
- Vault는 Wrapped SecretID를 반환
- 워커는 작업 러너를 생성하고, Wrapped SecretID를 변수로 전달
- 러너 컨테이너는 Wrapped SecretID의 unwrap을 요청
- Vault는 SecretID를 반환
- 러너는 RoleID와 SecretID를 사용해 Vault에 인증
- Vault는 필요한 시크릿 정보를 읽을 수 있는 정책이 포함된 토큰을 반환
- 러너는 이 토큰을 사용해 Vault에서 시크릿을 가져옴
1. Jenkins에서 Vault Plugin 설치
- Jenkins UI 접속
- 상단 메뉴에서 Manage Jenkins → Plugins
- Available 탭에서 Vault 검색
- HashiCorp Vault Plugin 설치 후 Jenkins 재시작
2. Vault AppRole 정보 확인
Secret ID는 1시간 만료이므로, 그냥 다시 생성해서, 해당 값을 젠킨스에 설정하고 빌드 실습
- Vault에서 발급된 ROLE_ID, SECRET_ID는 이전에 생성한 role_id.txt secret_id.txt 값을 참고하여 사용할 수 있습니다.
# Role ID 확인 및 Secret ID 신규 발급
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"
3. Jenkins에서 Vault 설정 및 Credentials 추가
3.1 Jenkins UI(admi/qwe123) → Manage Jenkins → Configure System
4. Jenkins Pipeline Job 생성
- Jenkins UI → New Item → Pipeline 선택
- jenkins-vault-kv 입력 후 생성
- Jenkinsfile 작성
Jenkinsfile 예시
pipeline {
agent any
environment {
VAULT_ADDR = 'http://172.18.127.91: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}"
}
}
}
}
}
}
Jenkins 실행결과 → 보안상 취약하므로 마스킹처리됨.
- 유의사항
- KV Version1은 경로에 data을 넣고 Version2는 경로에 data을 넣지 않습니다! - 참고링크
- Version 1 : secret/data/sampleapp/config
- Version 2 : secret/sampleapp/config
- sh 블록 vs script 블록
- KV Version1은 경로에 data을 넣고 Version2는 경로에 data을 넣지 않습니다! - 참고링크
상황
|
사용 방식
|
sh 블록
|
$USERNAME, $PASSWORD
|
script 블록
|
${env.USERNAME}, ${env.PASSWORD}
|
ArgoCD + Vault Plugin (Kubernetes Auth/AppRole) - CD
Vault + ArgoCD Plugin 패턴
ArgoCD Vault Plugin
ArgoCD Vault Plugin 연동방안
docmoa.github.io
Secret Management with GitOps and Argo CD Vault Plugin
How to use the Argo CD Vault plugin to inject secrets into Kubernetes manifests
colinwilson.uk
ArgoCD Vault Plugin 소개
- Argo CD에는 다양한 시크릿 관리 도구(HashiCorp Vault, IBM Cloud Secrets Manager, AWS Secrets Manager 등)플러그인을 통해 Kubernetes 리소스에 주입할 수 있도록 지원합니다.
- 플러그인을 통해 Operator 또는 CRD(Custom Resource Definition)에 의존하지 않고 GitOps와 Argo CD로 시크릿 관리 문제를 해결할 수 있습니다.
- 특히 Secret 뿐만 아니라, deployment, configMap 또는 기타 Kubernetes 리소스에도 사용할 수 있습니다.
설치/구성관련 내용 요약(ChatGPT)
Argo CD Vault Plugin의 공식 문서에서는 Argo CD에 플러그인을 설치하는 방법으로 네 가지를 제시하고 있습니다:
- argocd-cm ConfigMap을 통한 설치:
- argocd-repo-server에 InitContainer를 추가하여 플러그인을 다운로드하고 설정합니다.
- 또는 플러그인이 사전 설치된 커스텀 이미지를 생성하여 사용합니다.
- 사이드카 컨테이너를 통한 설치:
- 사이드카 컨테이너를 추가하여 플러그인과 필요한 도구들을 포함시킵니다.
- 또는 플러그인이 사전 설치된 커스텀 사이드카 이미지를 생성하여 사용합니다.
이 중 사이드카 컨테이너를 활용한 방법은 Argo CD v2.4.0부터 도입된 최신 방식으로, 보안성과 유지보수 측면에서 권장됩니다.
1. ArgoCD Vault Plugin을 위한 Credentials 활성화 - AppRole 인증
이전 실습에서 획득한 Role_ID, Secret_ID활용
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: ac1035aa-8ec2-ee64-ade3-905544f2cce7 #Role_ID
AVP_SECRET_ID: b4dab5f9-e0f8-6a0e-07eb-c616848d796c #Secret_ID
EOF
2. ArgoCD Vault Plugin 설치
참고사항
- ArgoCD Vault Plugin 설치 방법은 2가지가 있으며 현재는 Installation via a sidecar container 방식을 사용하는 것을 권장합니다. (이번 실습에서는 편의상 Helm으로 배포한 ArgoCD에 Kustomize을 활용해 기존 YAML에 Patch 적용)
- ArgoCD Helm 설치시 Plugin 활성화 방안
git clone https://github.com/hyungwook0221/argocd-vault-plugin.git
cd argocd-vault-plugin/manifests/cmp-sidecar
# 예전 문법으로 적용된 부분을 edit fix 명령으로 현행화
# kustomize edit fix
# argocd 네임스페이스 설정
kubens argocd
# 생성될 메니페스트 파일에 대한 확인
kubectl kustomize .
# -k 옵션으로 kusomize 실행
kubectl apply -n argocd -k .
3. 샘플 Application 배포하여 Vault와 동기화
에러로 인해 배포가 제대로 되지 않는다 ㅠㅠ..
정상 배포 시 위와 같은 화면이다.
Deployment에 적용된 env 값 확인
envs:
- name: VAULT_SECRET_USER
value: <path:secret/data/sampleapp/config#username>
- name: VAULT_SECRET_PASSWORD
value: <path:secret/data/sampleapp/config#password>
Vault Secrets Operator (VSO)
- Vault Secrets Operator(이하, VSO)에 대한 주요 개념과 구성요소를 알아본다.
- VSO을 통해서 Vault을 통해 획득한 시크릿을 Kubernetes Secret을 자동으로 동기화하고 관리하는 방법을 알아본다.
VSO란?
Vault Secrets Operator(VSO)는 Kubernetes Secrets에서 Vault secrets 및 HCP Vault Secrets Apps를 네이티브하게 사용할 수 있도록 Pods에 제공해줍니다.
개요
Vault Secrets Operator는 지원하는 Custom Resource Definitions(CRD) 집합의 변경 사항을 감시하여 작동합니다.
각 CRD는 시크릿의 지원되는 소스 중 하나에서 Kubernetes Secret으로 동기화할 수 있도록 필요한 사양을 제공합니다.
오퍼레이터는 소스 시크릿 데이터를 대상 Kubernetes Secret에 직접 작성하며, 소스에 변경 사항이 발생할 경우 해당 내용을 대상에도 수명 주기 동안 지속적으로 반영합니다.
이렇게 함으로써 애플리케이션은 대상 시크릿에만 접근하면 그 안의 시크릿 데이터를 사용할 수 있습니다.
기능
Vault Secrets Operator가 지원하는 주요 기능은 다음과 같습니다:
- 여러 시크릿 소스로부터의 동기화 지원
- 자동 시크릿 일탈 감지 및 수정
- Deployment, ReplicaSet, StatefulSet Kubernetes 리소스 유형에 대한 자동 시크릿 교체
- 오퍼레이터 모니터링을 위한 Prometheus 전용 계측 지원
- Helm 또는 Kustomize를 통한 설치 지원
- 시크릿 데이터 변환 지원
지원되는 Kubernetes 배포판
- Amazon Elastic Kubernetes Service (EKS)
- Google Kubernetes Engine (GKE)
- Microsoft Azure Kubernetes Service (AKS)
- Red Hat OpenShift (공식 인증됨)
- Vault Secrets Operator는 다음과 같은 호스팅된 Kubernetes 환경에서 성공적으로 테스트되었습니다:
VSO 구성도 - 개발자(Developer) / 운영자(Operator)의 역할
VSO CRD 관계도
Dynamic Secrets
1. VSO 배포를 위한 Chart Values 파일 작성
# 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"]
- Values 구성요소 분석
- defaultVaultConnection: 기본 Vault 연결 설정을 구성
- address: vault.vault.svc.cluster.local:8200 (Helm으로 설치된 Vault의 ClusterIP)
- skipTLSVerify: false: TLS 검증 비활성화 안 함 (Vault에 TLS 적용된 경우 주의 필요)
- clientCache 설정
- persistenceModel: direct-encrypted: 암호화된 데이터를 직접 저장
- storageEncryption.enabled: true: 암호화 기능 활성화
- transitMount: demo-transit: 앞에서 Vault에 만든 Transit 엔진 mount path
- keyName: vso-client-cache: Transit에서 사용하는 암호화 키
- mount: k8s-auth-mount: Kubernetes 인증 방식에서 사용할 auth path
- kubernetes.role: auth-role-operator 라는 Role에 지정된 정책으로 암복호화 가능
- serviceAccount: VSO가 사용하는 ServiceAccount 이름
- tokenAudiences: Vault 서버에서 사용하는 audience 설정
- 위와 같은 설정으로 VSO는 Vault에 인증된 후, 동적/정적 시크릿을 Kubernetes 리소스로 가져오고, 가져온 시크릿 데이터를 자체적으로 암호화해 캐시 가능
- defaultVaultConnection: 기본 Vault 연결 설정을 구성
2. VSO 배포
helm install vault-secrets-operator hashicorp/vault-secrets-operator \
-n vault-secrets-operator-system \
--create-namespace \
--values vault-operator-values.yaml
#
kubectl get-all -n vault-secrets-operator-system
#
kubectl get pod -n vault-secrets-operator-system
#
kubectl rbac-tool lookup vault-secrets-operator-controller-manager
#
kubectl rolesum vault-secrets-operator-controller-manager -n vault-secrets-operator-system
#
kubectl get vaultauth -n vault-secrets-operator-system vault-secrets-operator-default-transit-auth -o jsonpath='{.spec}' | jq
3. PostgreSQL 설치 (Bitnami Helm Chart)
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'"
4. Kubernetes Auth Method 설정
#
kubectl exec -it vault-0 -n vault -- cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
-----BEGIN CERTIFICATE-----
MIIDBTCCAe2gAwIBAgIIfA5oV86RmOswDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE
AxMKa3ViZXJuZXRlczAeFw0yNTA0MTAwMDUzMTdaFw0zNTA0MDgwMDU4MTdaMBUx
EzARBgNVBAMTCmt1YmVybmV0ZXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQDSUCHyBXVhBb91uZKdqwFAussfSxqPadYhrj659fp/XuJQrRrOMLZAh5eP
6MCkeYbHeMY2stwBzuwzSzyaxkJdKiR5yKhtIHT7vpYn4XT0bNdyWWKAHkjyqjsk
DYkAbwQ1xksL3DwArRfUSdZPCVsDLhu+G3wbGKiW64q0Rb1pEk/gK2AhxnSSqoeR
YNzh10FA/CxS2wiLFfsm8NAn4KHG134AKY0Vxb/EkXnL9b2jRK+BfSh/sSpuEzN3
16Xy1fC5UX/xCdTvwx+57xOoFhymaF0aVISSavzrkaYd8kaPwAKtoAlxvDmDrVhv
YKRfCTerHUCOL3IPQqChIZQIUvoVAgMBAAGjWTBXMA4GA1UdDwEB/wQEAwICpDAP
BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSDK0FYEL+FNdgIpMhCnj9g2CkAIjAV
BgNVHREEDjAMggprdWJlcm5ldGVzMA0GCSqGSIb3DQEBCwUAA4IBAQC5/EdWTRD6
SoghjPe/bKKfQzFxPmEZVTb0JI0CWVUZorGzFcHCMCXMOf/XuECkK5tqn7QR/Nw8
yy3XvmT6DY+J2GshrusJXfMEOJ9RQIwDBJKHoZqbcyI/w294Ecp1Mpivb8WeI2FW
wMsaDoswQ3H/0zrEBwhhpUAWcmjm6w4/7Tsid9t1DsQrp9xH37wU/uP07HyEIKPS
TCQ2tUueHSVihNhCg7RejjbiGy8EUZV6aGIrtFgss9Mzk+5m+1KXm8o+Lj/YNYFx
JFeII2h3ZptO4I9vHrohXKVw0esHNzdHQPeGEXCWHTd+K38jIJPfggA8Yu1htbUa
MKEIbcT53UHH
-----END CERTIFICATE-----
# 해당 ca.crt 는 k8s 의 루트인증서 정보
kubectl exec -it vault-0 -n vault -- cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt > vso-ca.crt
openssl x509 -in vso-ca.crt -noout -text
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 8939197036714105067 (0x7c0e6857ce9198eb)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=kubernetes
...
# vault 서비스 어카운트의 token 확인
kubectl exec -it vault-0 -n vault -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsImtpZCI6InJBT1dvdDVZME1wMjZ4NmJuZkVvNk9nc1BGY1FIVDgyaUllUy1VN05oejgifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzc1ODAzNjMyLCJpYXQiOjE3NDQyNjc2MzIsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiZTJkOTdiYTItNTMzMi00ZDFlLWJjYWMtYzg3NDdmNGQ2MWIwIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJ2YXVsdCIsIm5vZGUiOnsibmFtZSI6Im15azhzLWNvbnRyb2wtcGxhbmUiLCJ1aWQiOiI5ZGZkYTg5YS1hYzZkLTRmNTYtYjlhOC03MTg2YTI3MWVjZjMifSwicG9kIjp7Im5hbWUiOiJ2YXVsdC0wIiwidWlkIjoiNGFlMTVhZTctOTI0NS00OTZkLWE0ZDItYWI3ZTk4NDA1Yjg1In0sInNlcnZpY2VhY2NvdW50Ijp7Im5hbWUiOiJ2YXVsdCIsInVpZCI6ImNjYjgwNjJkLTQ4OTctNDI1MC1hYzg3LTYxMTAxYWQ0YjI4NyJ9LCJ3YXJuYWZ0ZXIiOjE3NDQyNzEyMzl9LCJuYmYiOjE3NDQyNjc2MzIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDp2YXVsdDp2YXVsdCJ9.dekUKWseyWNgYO4uUi1MHI16BvHOAmeO61gxO-LIEK7m5ahG72wLwUnl5G22eRlA8T9X2DnvB8tIPyGDawzkCJWve7PTq-_76QOXZO8a3-KUY9kJ3gI-GymN9jwV9eUxXoFGqp73WxZCK-derP6aj5WZirSCHb1YQnioKmxQ4frhQVYi-GDLLz6A2OH-MnZIM68LvFPxK06Id2McMlqOFnV3JCDSzxvyEtnANQgdNWXIit-o6FFzDvKhlfwN6Yrjs9AO7JEyFeQy7M-C2_mQq_Xqht-cO28b8tA4kEJLIdWSIfu1LAWP06YZYeEbQM-8qZ9HoXR1L-QFtonMLkJ1pg
# https://jwt.io/ 에서 token 디코드 확인 시 PAYLOAD 부분
{
"aud": [
"https://kubernetes.default.svc.cluster.local"
],
"exp": 1775803632,
"iat": 1744267632,
"iss": "https://kubernetes.default.svc.cluster.local",
"jti": "e2d97ba2-5332-4d1e-bcac-c8747f4d61b0",
"kubernetes.io": {
"namespace": "vault",
"node": {
"name": "myk8s-control-plane",
"uid": "9dfda89a-ac6d-4f56-b9a8-7186a271ecf3"
},
"pod": {
"name": "vault-0",
"uid": "4ae15ae7-9245-496d-a4d2-ab7e98405b85"
},
"serviceaccount": {
"name": "vault",
"uid": "ccb8062d-4897-4250-ac87-61101ad4b287"
},
"warnafter": 1744271239
},
"nbf": 1744267632,
"sub": "system:serviceaccount:vault:vault"
}
# vault SA 토큰 만료 기간 : 대략 1시간(3600초)+7초
k get pod -n vault vault-0 -o yaml
...
- name: kube-api-access-9rqgv
projected:
defaultMode: 420
sources:
- serviceAccountToken:
expirationSeconds: 3607
path: token
- configMap:
items:
- key: ca.crt
path: ca.crt
name: kube-root-ca.crt
kubectl exec --stdin=true --tty=true vault-0 -n vault -- /bin/sh
----------------------------------------------------------------
# 최초 설치시 획득한 vault root token 사용.
# 예시) hvs.hnlFKWjE10FOyrwLMeK9PrCC
vault login
# Kubernetes 인증 메서드 활성화
vault auth enable -path k8s-auth-mount kubernetes
#
vault auth list
Path Type Accessor Description Version
---- ---- -------- ----------- -------
approle/ approle auth_approle_03723e90 n/a n/a
k8s-auth-mount/ kubernetes auth_kubernetes_a2ee4150 n/a n/a
token/ token auth_token_de4f4e43 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
Key Value
--- -----
disable_iss_validation true
disable_local_ca_jwt false
issuer n/a
kubernetes_ca_cert -----BEGIN CERTIFICATE-----
MIIDBTCCAe2gAwIBAgIIfA5oV86RmOswDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE
AxMKa3ViZXJuZXRlczAeFw0yNTA0MTAwMDUzMTdaFw0zNTA0MDgwMDU4MTdaMBUx
EzARBgNVBAMTCmt1YmVybmV0ZXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQDSUCHyBXVhBb91uZKdqwFAussfSxqPadYhrj659fp/XuJQrRrOMLZAh5eP
6MCkeYbHeMY2stwBzuwzSzyaxkJdKiR5yKhtIHT7vpYn4XT0bNdyWWKAHkjyqjsk
DYkAbwQ1xksL3DwArRfUSdZPCVsDLhu+G3wbGKiW64q0Rb1pEk/gK2AhxnSSqoeR
YNzh10FA/CxS2wiLFfsm8NAn4KHG134AKY0Vxb/EkXnL9b2jRK+BfSh/sSpuEzN3
16Xy1fC5UX/xCdTvwx+57xOoFhymaF0aVISSavzrkaYd8kaPwAKtoAlxvDmDrVhv
YKRfCTerHUCOL3IPQqChIZQIUvoVAgMBAAGjWTBXMA4GA1UdDwEB/wQEAwICpDAP
BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSDK0FYEL+FNdgIpMhCnj9g2CkAIjAV
BgNVHREEDjAMggprdWJlcm5ldGVzMA0GCSqGSIb3DQEBCwUAA4IBAQC5/EdWTRD6
SoghjPe/bKKfQzFxPmEZVTb0JI0CWVUZorGzFcHCMCXMOf/XuECkK5tqn7QR/Nw8
yy3XvmT6DY+J2GshrusJXfMEOJ9RQIwDBJKHoZqbcyI/w294Ecp1Mpivb8WeI2FW
wMsaDoswQ3H/0zrEBwhhpUAWcmjm6w4/7Tsid9t1DsQrp9xH37wU/uP07HyEIKPS
TCQ2tUueHSVihNhCg7RejjbiGy8EUZV6aGIrtFgss9Mzk+5m+1KXm8o+Lj/YNYFx
JFeII2h3ZptO4I9vHrohXKVw0esHNzdHQPeGEXCWHTd+K38jIJPfggA8Yu1htbUa
MKEIbcT53UHH
-----END CERTIFICATE-----
kubernetes_host https://kubernetes.default.svc:443
pem_keys []
token_reviewer_jwt_set true
use_annotations_as_alias_metadata false
----------------------------------------------------------------
vault write 명령 분석
- Vault가 K8s API 서버에 인증 요청을 보낼 수 있도록 설정
- kubernetes_host: API 서버 주소
- kubernetes_ca_cert: API 서버의 CA 인증서 (Pod 내 ServiceAccount에서 자동 주입됨)
- token_reviewer_jwt: TokenReviewer 권한이 있는 토큰으로 Vault가 요청자의 토큰을 검증할 수 있도록 설정
5. Vault Kubernetes Auth Role 설정
실행 중인 애플리케이션이 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
# auth-role 생성
vault write auth/k8s-auth-mount/role/auth-role \
bound_service_account_names=demo-dynamic-app \ # demo-dynamic-app 서비스 어카운트를 사용하는 Pod에서만 인증 허용, Pod 내부에서 Vault에 로그인하려면 이 서비스 어카운트를 써야 함
bound_service_account_namespaces=demo-ns \ # 이 Role은 오직 demo-ns 네임스페이스에서 오는 요청만 허용
token_ttl=0 \ # 발급되는 Vault 토큰의 기본 TTL 없음(0). 일반적으로 token_ttl=0이면 token_period가 사용됨
token_period=120 \ # Lease 기간이 120초인 Renewable Token 발급. TTL이 없는 대신, 2분마다 갱신 가능한 토큰을 생성. 보통 이 설정은 "periodic token"이라 불리며, long-lived session에 적합.
token_policies=demo-auth-policy-db \ # 인증에 성공하면 발급되는 토큰에 demo-auth-policy-db 정책이 적용
audience=vault # JWT 토큰의 aud (Audience) claim 검증에 사용.
6. Vault Database Secret Engine 설정
# 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"
# 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"
# 정책 설정: 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
7. Transit Secret Engine 설정 + VSO 연동 Role 구성
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
# 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
8. 샘플 애플리케이션 YAML 작성 및 배포
demo-ns 네임스페이스 생성
kubectl create ns demo-ns
mkdir vso-dynamic
cd vso-dynamic
vault-auth-dynamic.yaml :
- 앱이 Vault에 인증하기 위한 ServiceAccount 및 VaultAuth 리소스
- Vault에서 발급한 동적 PostgreSQL 크레덴셜을 얻기 위해 반드시 필요
---
# 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가 소스 시크릿 데이터를 동기화 하는 주기 설정가능 → Event Notification 기능을 통해 변경이 있을 경우 즉시 반영할 수도 있음
---
# 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
9. 동작확인(UI) : postgres-postgresql.postgres.svc , db는 postgres
localhost:30003 접속 후 K8s Secrets에 생성된 username, password 값으로 연결 테스트
PKI Secrets
- 운영자가 단일 Vault PKI 시크릿을 단일 Kubernetes 시크릿으로 동기화하는 데 필요한 구성입니다.
- Ingress, mTLS, Webapp 등 PKI 인증서를 사용하는 애플리케이션에서 동적으로 변경된 PKI 인증서를 사용할 수 있도록 지원합니다.
1. Vault PKI 시크릿엔진 사용을 위한 설정
- PKI 엔진 활성화(마운트)
vault secrets enable -path=pki pki
Vault에 PKI Secrets Engine을 pki라는 사용자 정의 경로로 마운트합니다.
- Root CA(Common Name) 인증서 생성(발급)
vault write pki/root/generate/internal \
common_name="hashicorp.local" \
ttl=87600h
Vault 자체가 루트 인증기관(CA)이 됨
Root CA 인증서와 개인키를 Vault 내부에 생성·보관 - 10년 TTL
PEM 형식으로 루트 인증서 반환됨 (출력 포함)
공개 인증서/CRL URL도 함께 설정해야 함 (config/urls 경로 설정 생략됨 – 권장 추가)
- Issuing Cert & CRL Distribution URL 설정
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"
Vault가 발급하는 인증서에 포함될 CA/CRL 메타데이터
- 인증서 발급용 Role 생성
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"
hashicorp-app.svc.cluster.local 도메인 네임에 대해 PKI 엔진에 요청 허용
인증서 요청에 대해 세밀하게 제어가능한 여러 옵션이 있으나, 간이 테스트 목적이므로 생략
- 인증서 발급용 Policy 정의
vault policy write pki-policy - <<EOF
path "pki/issue/cert-role" {
capabilities = ["update", "read", "list"]
}
EOF
- Role 생성과 함께 policy 바인딩
vault write auth/approle/role/cert-role \
token_policies="pki-policy" \
token_ttl="30m" \
token_max_ttl="1h"
- Role ID / Secret ID 발급 및 K8s Secret에 저장
# Local에서 실행
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
2. VSO의 CR(Custom Resource) 배포
- webapp 네임스페이스 생성
kubectl create ns webapp
- AppRole Secret 생성
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: <Role ID>
secretRef: vso-cert-role
EOF
- VaultPKISecret CR 생성
# 1. vault agent 역할. 사용자를 대행하여 vault에 주기적 요청을 통해 인증서를 갱신
# 2. 인증서를 사용하는 devployment에 대해 롤 아웃 수행하여 바뀐 인증서 정보 반영
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
인증서 갱신 동작 로직의 이해
- 인증서의 NotAfter (만료일) 확인
- 현재 시간과 비교해서 남은 시간이 ⅓ 미만일 때부터 재발급 트리거
- 단, 실제 갱신은 Controller의 reconcile 주기에 따라 수 초 단위로 오차가 있을 수 있음
- VSO는 VaultPKISecret 리소스를 watch 하면서 아래 조건을 기준으로 자동 갱신 시점을 판단합니다
설정 TTL | 갱신 예상 시점 (남은 시간) |
1m | 약 20초 후 |
10m | 약 3~4분 후 |
1h | 약 20분 후 |
3. 생성된 인증서 확인
kubectl get secret vso-pki-cert -n webapp -o json | jq -r .data._raw | base64 -d
# view-secret 플러그인
kubectl view-secret -n webapp vso-pki-cert --all
# 만료기간 확인 Tip (MacOS 기준)
kubectl view-secret -n webapp vso-pki-cert expiration
1743932886
# date 명령으로 확인
date -r 1743948834
Sun Apr 6 18:45:08 KST 2025
4. 검증을 위한 서비스(Nginx) 배포
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
'k8s' 카테고리의 다른 글
K8S CI/CD (2) | 2025.03.29 |
---|---|
[k8s] 쿠버네티스 스토리지, 보안 관리(PV/PVC, RBAC, 모니터링) (0) | 2024.12.01 |
[k8s]쿠버네티스 워크로드, 네트워킹 (0) | 2024.11.24 |
[k8s] 쿠버네티스 주요 컴포넌트 정리 (1) | 2024.09.22 |