I got IT

K8s 시크릿 관리 with Vault 본문

AWS/EKS

K8s 시크릿 관리 with Vault

joshhoxy 2025. 4. 13. 05:02

※ 해당 글은 Hashicorp 유형욱 님의 강의와 가시다님의 AEWS 스터디 내용을 참고하여 작성하였습니다.

 

들어가며

현대 애플리케이션은 수많은 비밀정보(Secrets)에 의존합니다. 데이터베이스 사용자 인증 정보, 외부 API 키, 클라우드 액세스 토큰, 인증서 등… 어느 하나라도 유출되면 서비스 중단, 데이터 손실, 보안 사고로 이어질 수 있습니다. 특히 쿠버네티스(Kubernetes) 환경에서는 수많은 마이크로서비스가 동적으로 생성되고 사라지기 때문에, 이러한 시크릿을 안전하게 저장하고 주입하는 방식이 매우 중요해졌습니다.

이를 위해 등장한 것이 바로 HashiCorp Vault입니다. Vault는 민감 정보를 중앙에서 안전하게 저장하고, 접근 제어, 시크릿 주입, 자동 갱신(Key Rotation)까지 지원하는 시크릿 관리 시스템입니다. 쿠버네티스와도 자연스럽게 통합되어, Pod가 시작될 때 자동으로 필요한 시크릿을 주입받을 수 있도록 도와줍니다.

이번 포스팅에서는 Kubernetes 환경에서 Vault를 활용한 안전한 시크릿 관리 방식을 소개하고, 실제 예제와 함께 그 흐름을 하나씩 살펴보겠습니다.

실습 환경 구성

저의 경우는 EC2를 한대 띄워서 실습을 진행하였습니다.

- OS: Ubuntu22.04

- 2vcpu 8G MEM

docker 설치

※ docker, docker-compose 설치가 안되어있다면

# 1. 기존 docker 관련 패키지 제거 (선택적)
sudo apt remove docker docker-engine docker.io containerd runc

# 2. 필수 패키지 설치
sudo apt update
sudo apt install ca-certificates curl gnupg lsb-release -y

# 3. Docker GPG 키 추가
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

# 4. Docker 리포지토리 추가
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 5. Docker 설치
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin -y

kind 설치

# kind 바이너리 다운로드 및 설치
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
kind version

k8s 관리 도구 설치

kubectl 설치

# 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

 

Helm 설치

# Install Helm
curl -s https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
helm version

 

kubectl 명령어 자동완성 세팅

# 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 적용을 위해서 logout 후 터미널 다시 접속 하자
exit

 

krew 설치

(
  set -x; cd "$(mktemp -d)" &&
  OS="$(uname | tr '[:upper:]' '[:lower:]')" &&
  ARCH="$(uname -m | sed -e 's/x86_64/amd64/' -e 's/\(arm\)\(64\)\?.*/\1\2/' -e 's/aarch64$/arm64/')" &&
  KREW="krew-${OS}_${ARCH}" &&
  curl -fsSLO "https://github.com/kubernetes-sigs/krew/releases/latest/download/${KREW}.tar.gz" &&
  tar zxvf "${KREW}.tar.gz" &&
  ./"${KREW}" install krew
)

echo "export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"" >> ~/.bashrc
source ~/.bashrc

# krew 설치 확인
kubectl krew

 

k9s 설치 🔗

# os에 맞게 설치(ubuntu22.04)
wget https://github.com/derailed/k9s/releases/tag/v0.50.0
tar -zxvf k9s_Linux_amd64.tar.gz
mv k9s /usr/local/bin/

k9s

kubectx 설치

sudo git clone https://github.com/ahmetb/kubectx /opt/kubectx
sudo ln -s /opt/kubectx/kubectx /usr/local/bin/kubectx
sudo ln -s /opt/kubectx/kubens /usr/local/bin/kubens

보안 레벨 강등

sudo systemctl stop apparmor && sudo systemctl disable apparmor

 

클러스터 생성

사전 준비

mkdir cicd-labs
cd ~/cicd-labs

# 네트워크 도구 설치
apt install net-tools -y

# 로컬 IP 확인
ifconfig ens5 | grep inet

# 변수 저장
MyIP=<your-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
  - containerPort: 30004
    hostPort: 30004
  - containerPort: 30005
    hostPort: 30006
- role: worker
- role: worker
EOF
**kind create cluster --config kind-3node.yaml --name myk8s** --image kindest/node:v1.32.2

 

컨트롤플레인 노드 1대 워커노드 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

 

kube-ops-view 설치

배포 확인 편의를 위해 kube-ops-view를 설치합니다. helm 으로 설치합니다.

helm repo add geek-cookbook https://geek-cookbook.github.io/charts/
helm install kube-ops-view geek-cookbook/kube-ops-view --version 1.2.2 --set service.main.type=**NodePort**,service.main.ports.http.nodePort=**30001** --set env.TZ="Asia/Seoul" --namespace kube-system

 

설치 확인

kubectl get deploy,pod,svc,ep -n kube-system -l app.kubernetes.io/instance=kube-ops-view

 

curl ipconfig.io/ip 조회하여 퍼블릭 IP로 접근. 이 때 EC2의 보안그룹도 열려있어야 합니다.

 

Jenkins 설치

kind 네트워크에 Jenkins와 gogs 컨테이너를 올립니다.

docker network ls

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

  gogs:
    container_name: gogs
    image: gogs/gogs
    restart: unless-stopped
    networks:
      - kind
    ports:
      - "10022:22"
      - "3000:3000"
    volumes:
      - gogs-data:/data

volumes:
  jenkins_home:
  gogs-data:

networks:
  kind:
    external: true
EOT

docker compose up -d

 

 

docker 컨테이너 상태를 확인합니다.

docker compose ps
**docker inspect kind**

 

각 컨테이너의 기본 정보를 확인합니다.

for i in gogs jenkins ; do echo ">> container : $i <<"; docker compose exec $i sh -c "**whoami && pwd"**; echo; done

 

젠킨스 초기 설정

젠킨스 초기 암호 확인

docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

 

출력된 패스워드로 본인 서버 8080 포트에 접속하여 해당 패스워드를 입력합니다.

최초 로그인이 성공하면 admin 유저에 대한 패스워드를 지정합니다. qwe123

 

권장 플러그인을 설치해줍니다.

 

Argo CD 설치

argocd 네임스페이스 생성. argocd를 위한 네임스페이스를 생성하여 환경을 격리합니다.

kubectl create ns argocd

argocd 를 helm으로 설치할 예정입니다. helm의 value를 미리 작성하여 둡니다.

cat <<EOF > argocd-values.yaml
dex:
  enabled: false

server:
  service:
    type: NodePort
    nodePortHttps: 30002
  extraArgs:
    - --insecure  # HTTPS 대신 HTTP 사용
EOF

보안그룹도 미리 30002 포트를 열어둡니다.

helm repo를 등록하고 install 하여 설치합니다.

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

 

Argo CD 설치확인

# 확인
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

secret을 조회하여 최초 접속 암호를 확인합니다.

kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d ;echo

저 같은 경우에는 원격서버 EC2(ubuntu os)에서 브라우저가 지원되지 않는 환경이기 때문에 터널링을 통해 원격서버의 argocd에 접근하여 주었습니다.

 

ssh 터널링

ssh -L 8000:localhost:30002 user@원격서버IP

 

위에서 secret을 조회하여 확인한 패스워드와 admin 사용자를 입력하여 접속합니다.

패스워드를 변경해줍니다. 좌측 상단의 user info를 클릭하여 변경합니다. (qwe12345)

👉 이후 실습에서 ArgoCD Vault Plugin 추가를 진행할 계획입니다!

 

Vault

Vault 란?

HashiCorp Vault는 신원 기반 인증과 인가를 통해 비밀(시크릿)과 암호화 키를 안전하게 관리하는 시스템입니다. API 키, 비밀번호, 인증서 등 민감한 정보를 중앙에서 통제하고 감사 가능하게 관리하며, 접근 제어와 키 롤링 같은 보안 기능도 제공합니다.

즉 Vault는 쉽게 말해서 비밀번호나 API 키 같은 민감한 정보를 안전하게 보관하고 꺼내 쓰게 해주는 금고 같은 시스템입니다.

예를 들어, 우리 서비스가 DB 접속할 때 ID/PW를 써야 한다면 그걸 그냥 환경변수에 두는 게 아니라 Vault에 넣어두고, 서비스가 필요할 때만 꺼내쓰게 만드는 구조라고 생각하면 됩니다.

게다가 누가 언제 꺼냈는지도 다 로그로 남기고, 주기적으로 키를 바꾸는 키 롤링도 자동으로 해줘서 보안 관점에서도 굉장히 뛰어납니다.

예를 들면 “이 비밀을 이 앱만 볼 수 있게 하고, 다른 앱은 못 보게 해줘” 같은 설정도 가능해서, 서비스 간 인증 관리가 훨씬 깔끔해지고 손쉬워 지게 됩니다.

Vault의 동작 방식

Vault의 핵심 워크플로우는 다음 네 단계로 구성됩니다:

  1. 인증 (Authenticate): Vault에서 인증은 클라이언트가 Vault에 자신이 누구인지 증명할 수 있는 정보를 제공하는 과정입니다. 클라이언트가 인증 메서드를 통해 인증되면, 토큰이 생성되고 정책과 연결됩니다.
  2. 검증 (Validation): Vault는 Github, LDAP, AppRole 등과 같은 신뢰할 수 있는 외부 소스를 통해 클라이언트를 검증합니다.
  3. 인가 (Authorize): 클라이언트는 Vault의 보안 정책과 비교됩니다. 이 정책은 Vault 토큰을 사용하여 클라이언트가 접근할 수 있는 API 엔드포인트를 정의하는 규칙의 집합입니다. 정책은 Vault 내 특정 경로나 작업에 대한 접근을 허용하거나 거부하는 선언적 방식으로 권한을 제어합니다.
  4. 접근 (Access): Vault는 클라이언트의 신원에 연관된 정책을 기반으로 토큰을 발급하여 비밀, 키, 암호화 기능 등에 대한 접근을 허용합니다. 클라이언트는 이후 작업에서 해당 Vault 토큰을 사용할 수 있습니다.

아래 그림에서 Vault의 핵심 워크플로우를 확인할 수 있습니다.

Vault의 Token 기반 방식

Vault는 주로 토큰(Token)을 기반으로 작동하며, 이 토큰은 클라이언트의 정책(Policy)과 연결되어 있습니다. 각 정책은 경로(path) 기반으로 설정되며, 정책 규칙은 클라이언트가 해당 경로에서 수행할 수 있는 작업과 접근 가능성을 제한합니다.

Vault에서는 토큰을 수동으로 생성해 클라이언트에 할당할 수도 있고, 클라이언트가 로그인하여 토큰을 직접 획득할 수도 있습니다.

Vault 한눈에 살펴보기

다음 표에 Vault의 기능과 주요 목적, 사례등을 가시다님께서 일목요연하게 정리해주셨습니다.

특징 HashiCorp Vault
주요 기능 다양한 환경과 플랫폼에서 secret을 저장, 액세스 및 배포하기 위한 포괄적인 secret 관리 솔루션
암호화 높은 유연성과 보안을 위해 여러 암호화 백엔드(예: AWS KMS, Azure Key Vault, GCP KMS)를 지원하는 고급 암호화 메커니즘을 제공합니다.
접근 제어 역할, 경로 및 작업을 기반으로 세분화된 권한을 허용하는 세부적인 액세스 제어 정책(ACL)을 구현합니다.
동적 secret 데이터베이스, 클라우드 자격 증명, SSH 키와 같은 리소스에 대한 일시적이고 주문형 액세스를 제공하여 동적 secret 생성을 지원합니다.
감사 로깅 모든 작업과 액세스 요청을 추적하여 책임과 추적성을 보장하기 위한 포괄적인 감사 로그를 제공합니다.
완성 Kubernetes, Terraform, Jenkins 및 클라우드 공급자와 같은 광범위한 도구 및 서비스와의 광범위한 통합을 통해 광범위한 애플리케이션 범위가 가능합니다.
설치 복잡성 서버 배포 및 구성을 포함한 더 많은 설정 및 인프라가 필요하며 학습 곡선이 더 가파를 수 있습니다.
secret rotation 자동화된 secret rotation을 지원하여 secret이 정기적으로 업데이트되고 노출 위험이 최소화되도록 합니다.
API 및 CLI 다양한 작업을 위한 vault와의 광범위한 프로그래밍 액세스 및 상호 작용을 허용하는 풍부한 API 및 CLI를 제공합니다.
사용 사례 동적 secret 생성, 서비스로서의 암호화, 다양한 애플리케이션 및 환경에서의 안전한 데이터 저장을 포함한 광범위한 사용 사례에 적합합니다.
전개 최적의 성능과 보안을 위해 전용 인프라와 리소스가 필요한 독립 실행형 서비스로 배포됨
확장성 분산 환경에서 대량의 secret과 여러 클라이언트를 처리할 수 있는 높은 확장성을 위해 설계되었습니다.
버전 관리 secret 버전 관리를 지원하여 사용자가 시간 경과에 따라 다양한 버전의 secret을 추적하고 관리할 수 있도록 합니다.
오픈소스 추가 기능과 지원을 제공하는 엔터프라이즈 버전과 함께 오픈 소스 도구로 사용 가능

Vault 기본 구조 및 동작 원리

Vault의 동작원리

호텔 체크인 절차에 비유해 Vault의 동작방식을 이해해 볼 수 있습니다.

호텔 체크인 절차 상황에 비유하면 다음과 같은 동작으로 Vault도 키에 대한 권한을 획득합니다.

  1. 인증 및 카드키 신청
  2. 투숙객이 신분증이나 여권을 제시해 본인 인증을 합니다.
  3. 카드키 정책 등록
  4. 리셉션에서는 고객이 머무를 방(예: 208호)과 엘리베이터 접근 권한, 사용 기간(예: 3일)을 설정합니다.
  5. 카드키 발급
  6. 설정된 정책에 따라 카드키가 발급됩니다.
  7. 출입
  8. 고객은 카드키로 엘리베이터와 객실에 출입할 수 있습니다.

👉 카드키는 객실에 대한 임시 접근 권한을 가진 인증 토큰입니다.

  1. 인증 및 접근기 신청
  2. 사용자가 LDAP, Okta, Azure AD 같은 외부 인증 수단을 통해 Vault에 로그인합니다.
  3. 접근키 생성
    • 예: S3 접근 권한
    • 유효기간: 1일
    • 접근 범위: 특정 IP 대역
  4. Vault는 정책에 따라 필요한 클라우드 리소스(AWS S3 등)에 대한 임시 접근 키를 생성합니다.
  5. 접근기 발급
  6. Vault가 생성한 임시 접근 키를 사용자 또는 앱에게 전달합니다.
  7. 접근
  8. 사용자는 해당 키를 이용해 AWS, GCP, Azure 등의 클라우드 서비스에 접근합니다.

👉  Vault는 클라우드 리소스에 안전하게 접근할 수 있는 임시 자격증명(credential)을 발급하고 관리합니다.

Vault 구성요소

Vault는 내부적으로 다음과 같은 컴포넌트의 상호작용으로 동작합니다.

  • HTTP/S API: Vault는 REST 방식의 API 호출을 지원합니다.
  • Token Store: 인증된 클라이언트의 토큰 정보를 저장
  • Rollback Manager: 작업 실패 시 변경 내용을 롤백하는 기능 제공
  • Core: Vault의 핵심 로직 처리기 – 라우팅, 정책 적용, 만료 관리 등을 조정
  • Policy Store: 정책 정보를 저장하고 각 토큰의 권한을 정의
  • Expiration Manager: 토큰이나 임시 자격 증명의 만료 시점을 관리
  • Audit Broker: 감사 로깅 이벤트를 Audit Device로 전달하는 중계자
  • Audit Device: 로그를 파일, syslog 등 외부 시스템에 기록하는 장치
  • Path Routing: 클라이언트 요청을 알맞은 백엔드(Auth, Secret, System 등)로 라우팅
  • System Backend: Vault의 시스템 관련 기능 (예: 초기화, 시일 등)을 처리
  • Secret Engine: 비밀(Secrets)을 저장, 생성, 회전 등의 기능을 제공 (예: KV, DB, AWS 등)
  • Auth Method: 클라이언트를 인증하는 메커니즘 (예: Token, GitHub, LDAP, AWS 등)
  • Storage Backend: 실제 데이터를 저장하는 물리적 저장소 (예: 파일, Consul, S3 등)
  • Barrier: 저장된 데이터에 대한 암호화/복호화를 담당하는 보안 경계선

Vault 실습

지금까지 Vault의 대략적인 개념과 원리를 알아보았습니다. 이제 실제 실습을 통해 내부적으로는 어떻게 작동하는 지 실제 어떤 usecase에 적용이 가능한지 확인을 해보도록 합니다.

실습목표는 다음과 같습니다.

  • 실습용 K8s 환경(KinD)에 Vault를 Helm으로 설치
  • 설치된 Vault 서버에 접속하고 UI/CLI로 기본 기능을 확인
  • Vault의 동작 방식과 구조의 이해

Vault 실습 환경 구성

1️⃣ Step1. Helm을 사용한 Vault 배포 

Vault 관리를 위한 네임스페이스를 생성하고 Helm 차트를 조회합니다.

# 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

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
      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
  • TLS 없이 간단히 접근 (❗실무에서, 실제 운영 환경에서는 반드시 사용할 것을 권고합니다 ❗)
  • 단일 노드 Raft 저장소 구성
  • NodePort (30000)로 외부 UI 접근이 가능
  • Kubernetes 연동을 위한 Vault Injector 미리 준비

※Vault Injector 란?

Kubernetes 환경에서 시크릿을 Pod 내부에 자동으로 주입(inject) 해주는 컴포넌트입니다. 즉 Pod가 시작될 때 Vault에서 필요한 시크릿(API 키, DB 비밀번호 등)을 자동으로 가져다가 환경변수나 파일로 Pod 안에 넣어주는 역할”을 합니다. 이로 인해 시크릿을 따로 안 건드려도 알아서 잘 들어오게 해주도록 할 수 있습니다.

 

Helm을 통해 vault를 설치하도록 합니다.

helm upgrade vault hashicorp/vault -n vault -f override-values.yaml --install

 

작업 편의를 위해 기본 네임스페이스를 변경하고 배포를 확인합니다.

kubens vault
k get pods,svc,pvc

vault 파드와 vault-injector 파드가 생성된 것을 확인할 수 있습니다.

2️⃣ Step2. Vault 초기화 및 잠금해제

Vault 파드에 진입하여 vault 상태를 확인하는 Vault status 명령어를 수행해봅니다.

kubectl exec -ti vault-0 -- vault status

각 출력된 상태의 필드 항목은 다음과 같이 설명할 수 있습니다.

  • Seal Type: shamir이 방식은 Vault의 봉인을 해제하기 위해 여러 키 중 일부만 있어도 되는 구조를 만듭니다.
  • → Vault는 Shamir’s Secret Sharing 알고리즘을 사용하여 복구 키(Recovery Keys)를 여러 조각으로 나눠 생성합니다.
  • Initialized: false
  • → Vault가 아직 초기화되지 않았다는 의미입니다. 사용을 시작하려면 vault operator init 명령으로 초기화를 진행해야 합니다.
  • Sealed: true
  • → Vault는 현재 봉인(sealed) 상태이며, 초기화 이후에도 vault operator unseal 명령으로 복구 키를 사용해 봉인을 해제해야 정상적으로 작동합니다.
  • Total Shares: 0 / Threshold: 0
  • → 아직 초기화되지 않았기 때문에 복구 키 조각이 생성되지 않았으며, 봉인을 풀기 위한 키 개수(Threshold)도 정의되어 있지 않습니다.
  • Unseal Progress: 0/0
  • → 언실 진행률도 아직 의미 없는 상태입니다. 초기화가 되어야 해당 항목이 유효해집니다.
  • Storage Type: raft
  • → Vault는 데이터를 저장할 때 외부 스토리지 대신 내장된 Raft consensus 기반 스토리지를 사용 중입니다. 간단한 구성과 고가용성을 함께 지원합니다.
  • HA Enabled: true
  • → Vault가 고가용성 모드로 설정되어 있으며, 여러 노드 간 리더 선출이 가능한 구조라는 뜻입니다.

init-unseal.sh 을 사용하여 Vault Unseal 자동화 합니다.

 

※ Vault에서 Unseal이란?

Vault는 시작되자마자 자동으로 잠긴 금고처럼 동작합니다. 즉, Vault 서버가 꺼졌다 켜지거나 처음 설치되면, 기밀 데이터에 접근할 수 없도록 “봉인(sealed)” 상태로 시작됩니다. 이 상태에선 아무도 시크릿을 읽을 수도, 쓸 수도 없습니다. 이러한 sealed 상태를 해제하는 것이 열어주는 작업을 Unseal(언실) 이라고 합니다.

(Shamir Secret Sharing 방식이란?)

여담으로 샤미르라는 수학자, 보안전문가 가 만든 알고리즘이고 RSA 공개키 시스템을 발명한 사람이라고 합니다.

Shamir’s Secret Sharing 알고리즘은 비밀을 여러 조각으로 나눠서 일부 조각만으로도 복원할 수 있게 하는 분산 보안 기법입니다. 예를 들어, 5개 중 3개만 있어도 원래 비밀을 복구할 수 있도록 설정할 수 있습니다.

 

init-unseal.sh 스크립트를 작성합니다.

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

 

해당 스크립트 내용은 다음과 같습니다,

  1. Vault 초기화 실행
  2. → vault operator init 명령을 통해 1개의 복구 키와 루트 토큰을 생성합니다.
  3. 복구 키(Unseal Key)와 루트 토큰 추출 및 저장
  4. → 출력 결과에서 키를 추출해 각각 텍스트 파일로 저장합니다.
  5. Vault 봉인 해제
  6. → 복구 키를 사용해 vault operator unseal 명령으로 Vault의 잠금을 해제합니다.
  7. 결과 출력
  8. → 봉인 해제 완료 메시지와 함께 루트 토큰을 출력합니다.

Vault status 를 확인하여 unseal이 됐는지 확인합니다. 스크립트를 수행하기전 sealed: true 였던게 seald:false 로 변한것을 확인할 수 있습니다.

root 토큰은 스크립트에 의해 vault-root-token.txt 에 저장되어있습니다.

 

root 토큰을 복사하여 Vault 서버 초기 인증에 사용합니다.

 

다음은 vault에 로그인 하면 나타나는 메인 홈페이지입니다.

 

3️⃣ Vault 바이너리 설치 및 환경변수 설정

vault 설치 - docs

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

 

Vault를 사용하기 위한 기본 환경변수 설정 - docs

현재 Vault API를 호출하기 위해 설정된 엔드포인트는 localhost:30000 으로 등록해줍니다.

export VAULT_ADDR='http://localhost:30000'
export VAULT_DATA=/opt/vault/data
export VAULT_CONFIG=/etc/vault.d

 

설치 확인 및 인증

vault status
vault login

위에서 생성된 초기 root token 값을 입력하여 로그인합니다.

 

KV 시크릿 엔진 활성화 및 샘플 구성 실습

KV = Key-Vaule

key value 형태의 시크릿을 생성해보고 사용해보는 실습을 진행해봅니다. KV는 Vault에서 시크릿 데이터를 경로 기반으로 저장하는 간단하고 범용적인 시크릿 저장소 초기에 Vault를 사용할 땐 대부분 KV로 시작하게 됩니다.

Secret Engine은 이러한 KV와 같은 비밀(Secrets)을 저장, 생성, 회전 등의 기능을 제공 하는 드라이버 입니다. (예: KV, DB, AWS 등)

1️⃣ Step 1. 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

username=demo, password=p@ssw0rd의 샘플 시크릿을 sampleapp/config 경로에 저장합니다.

2️⃣ Step2. Vault UI : [Secrets Engine] 탭에 접속 후 [sampleapp - config] 접속하여 실제 저장된 Key / Value 확인

Vault UI에 접근하여 위에서 생성한 secret 을 확인해봅니다.

 

Secrets > secret > sampleapp > config

 

이러한 secret을 API로 호출하기 위한 경로를 UI에서 확인할 수 있습니다. Paths 탭을 누르면 해당 secret의 API 호출 경로가 표시됩니다.

 

또한 다음과 같이 CLI를 사용하여서도 조회할 수 있습니다.

vault kv get -mount="secret" "sampleapp/config"
vault kv get secret/sampleapp/config

Vault Sidecar 연동 (Vault Agent) - 링크1 링크2

이번에는 다음과 같은 내용을 실습해봅니다.

이제 실질적으로 vault를 쿠버네티스 환경에서 사용하기 위한 실습을 진행해봅니다.

Vault Agent Injector를 사용하여 Kubernetes Pod 내부에 Vault Agent를 자동으로 주입해줍니다. 이를 통해 어플리케이션이 Vault로부터 자동으로 비밀 정보를 받아올 수 있게 됩니다.

Vault 를 쿠버네티스 환경에서 사용하기 위해 다음과 같은 사전 준비가 필요합니다.

위와 같은 환경 구성을 k8s의 사이드카 방식으로 구현할 수 있습니다. 다음 이미지는 k8s 사이드카를 활용한 vault 사용 아키텍처 입니다,

kubernetes 환경에서 Vault의 Sidecar Injector 아키텍처를 통해 Pod 안으로 시크릿(Secret)을 자동으로 주입하는 전체 워크플로우 입니다. 이를 통해 어플리케이션이 Vault와 직접 통신하지 않아도 안전하게 시크릿을 받을 수 있게 됩니다.

 

k8s와 Vault의 통신 과정을 다음과 같은 이미지를 통해 확인해 볼 수 있습니다.

1️⃣ kubectl로 Pod 생성 요청

  • 사용자가 kubectl로 어플리케이션 Pod를 배포합니다.
  • 이 때, Pod Spec 안에 Vault 관련 어노테이션 (예: 어떤 시크릿을 주입받을지)이 포함됩니다.
  • Pod에는 Kubernetes의 Service Account도 연결되어 있어야 합니다.

2️⃣ Mutating Webhook에 의한 Pod 변형

  • Kubernetes의 Sidecar Injector 서비스는 Mutating Admission Webhook으로 동작합니다.
  • Pod 생성 요청을 가로채고, 자동으로 아래 두 개의 컨테이너를 주입합니다:
    • vault-agent-init (InitContainer)
    • vault-agent (Sidecar Container)

3️⃣ Init Container: Vault 로그인 및 토큰 발급

  • vault-agent-init은 Pod의 ServiceAccount JWT 토큰을 사용해 Vault에 로그인합니다.
  • Vault의 Kubernetes Auth Method를 통해 Vault Token을 발급받습니다.
  • 이 Vault Token은 이후 Sidecar에서 사용됩니다.

4️⃣ Sidecar Container: Vault로부터 시크릿 가져오기

  • vault-agent Sidecar는 발급받은 토큰으로 Vault의 Secret Engine에 접근합니다.
  • 사전에 정의된 Policy에 따라 시크릿을 안전하게 가져와 /vault/secrets 같은 경로에 마운트합니다.
  • 이 시크릿은 Application Container에서 파일로 읽을 수 있음 (volumeMount 형태)

5️⃣ Application Pod는 단순하게 파일로 시크릿 접근

  • 어플리케이션 컨테이너는 Vault를 전혀 몰라도 됩니다.
  • /vault/secrets/... 경로에서 시크릿을 파일처럼 읽을 수 있게 됩니다.

즉 위와 같은 과정을 통해 어플리케이션이 Vault와 직접 통신하지 않아도 안전하게 시크릿을 받을 수 있게 되는 것입니다.

1️⃣ Step1. Vault AppRole 방식 인증 구성 - AppRole 인증이란?

AppRole은 사람의 개입 없이 자동화된 시스템이 Vault에 접근할 수 있도록 설계된 인증 방식 입니다. Role ID + Secret ID를 조합해 Vault Token을 안전하게 발급받을 수 있습니다.

  • role_id: 애플리케이션을 식별하는 ID (공개 가능)
  • secret_id: 인증 시 필요한 비밀번호 같은 값 (보안 중요). 따라서 주기적으로 변경을 해주는 것이 권장 됩니다.

이전에 secret 엔진을 활성화 했던 것 처럼 vault에서 app role 인증 방식을 활성화 합니다.

vault auth enable approle || echo "AppRole already enabled"
vault auth list

 

AppRole이 접근할 수 있는 시크릿 범위/권한을 설정하기 위한 policy를 생성합니다

vault policy write sampleapp-policy - <<EOF
path "secret/data/sampleapp/*" {
  capabilities = ["read"]
}
EOF

vault policy list

 

AppRole을 생성합니다.

vault write auth/approle/role/sampleapp-role \
  token_policies="sampleapp-policy" \
  secret_id_ttl="1h" \
  token_ttl="1h" \
  token_max_ttl="4h"

이 AppRole은 secret_id_ttl=60m으로 설정되어 있어, Secret ID가 생성된 이후 60분 동안만 유효합니다. 이처럼 Secret ID의 TTL(Time To Live)이나 1회용 설정 등을 통해 보안성을 세밀하게 제어할 수 있으며, 일반적으로 사람보다는 자동화된 시스템이나 백그라운드 애플리케이션에서 시크릿 접근을 자동화할 때 주로 사용됩니다.

발급된 토큰은 기본적으로 1시간 동안 유효합니다. 그 후 vault token renew로 연장 가능 하지만 총 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)

 

이를 파일로 저장하여 보관합니다.

mkdir -p approle-creds
echo "$ROLE_ID" > approle-creds/role_id.txt
echo "$SECRET_ID" > approle-creds/secret_id.txt

 

또는 k8s 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 -

 

2️⃣ Step2. Vault Agent Sidecar 연동

Vault Agent는 vault-agent-config.hcl 설정을 통해 연결할 Vault의 정보와, Template 구성, 렌더링 주기, 참조할 Vault KV 위치정보 등을 정의합니다.

1. Vault Agent 설정 파일 작성 및 생성 (vault-agent-config.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

Vault Agent는 사이드카 형태로 애플리케이션 옆에 붙여서 토큰 자동 획득, 시크릿 파일 생성, 자동 갱신 등을 도와줍니다.

Vault Agent가 AppRole 인증을 통해 Vault에 자동 로그인하고, 시크릿을 주기적으로 템플릿으로 렌더링하여 파일로 출력하도록 configmap을 생성합니다.

해당 configmap은 vault agent의 config 파일로 사용되고 다음과 같은 설정을 세팅하도록 되어있습니다.

Vault로부터 시크릿(secret/data/sampleapp/config)을 가져와 HTML 파일로 렌더링해서 /etc/secrets/index.html 경로에 저장합니다. 이 템플릿은 20초마다 갱신되어 최신 시크릿을 반영합니다.

 

2. 샘플 애플리케이션 + 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

Vault Agent는 agent-config.hcl을 읽고 AppRole로 Vault에 로그인 합니다. 이 때 시크릿을 템플릿 렌더링하여 /etc/secrets/index.html로 출력하고 토큰도 /etc/vault-agent-token/token에 저장합니다.

Vault Agent는 Vault로부터 안전하게 시크릿을 가져와 HTML 파일로 변환해주고, nginx는 그 HTML 파일을 그대로 사용자에게 보여줍니다.

 

3. SVC 생성

서비스 노출을 위해 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

※ 기존에 kube-ops-view를 호스팅했다면 잠시 꺼줍니다. Vault실습에서는 사용하지 않습니다.

helm uninstall kube-ops-view -n kube-system

 

4. 생성된 컨테이너 확인

kubectl get pod -l app=nginx-vault-demo
kubectl describe pod -l app=nginx-vault-demo

nginx 컨테이너와 vault-agent-sidecar 컨테이너가 성공적으로 배포된 것을 확인할 수 있습니다.

 

볼륨 마운트를 확인해 봅니다.

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

 

vault agent의 Vault 관련 설정 데이터를 확인해봅니다.

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

Vault Agent는 /etc/vault/agent-config.hcl 경로의 설정 파일을 읽어 동작하기 때문에, 해당 파일이 없거나 접근 불가능하면 Vault Agent는 정상적으로 실행되지 않거나 시크릿 주입이 실패합니다.

 

Vault-agent가 불러온 시크릿 정보를 vault-agent 컨테이너에서 확인해 봅니다.

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

 

Nginx 파드는 이러한 정보를 같은 볼륨을 공유하기 때문에 Vault 인증을 직접수행하지 않고도 조회할 수 있습니다. 이러한 내용을 nginx 컨테이너에서 해당 시크릿을 조회하여 확인해 봅니다.

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

 

이는 실제 배포된 화면을 통해서도 확인 가능합니다.

 

5. KV 값 변경 후 확인

Vault의 secret/data/sampleapp/config 시크릿을 변경하면, Vault Agent의 템플릿 렌더링 기능으로 별도 수작업 없이 Nginx html에 값이 수정되고 화면에 자동으로 반영됩니다.

 

시크릿은 version으로 관리되기 때문에 값을 변경할 때는 새로운 버전을 생성하여 변경합니다.

새로운 버전에 new접두사를 추가합니다.

 

새로운 버전을 생성하면 Nginx 페이지에 반영이 된것을 확인할 수 있습니다.

이는 Vault Agent가 주기적으로 시크릿을 체크하기 때문입니다. 설정된 static_secret_render_interval = "20s"에 따라 Vault Agent는 20초마다 시크릿의 최신 값을 조회합니다.

Jenkins + Vault (AppRole) - CI

Vault는 젠킨스와도 연동되어 Vault KV Store에 저장한 username, password을 Jenkins을 활용해서 획득하는 방안으로도 사용할 수있습니다. CI 파이프라인에서 정적(Static) 시크릿을 외부에 저장하고 관리할 경우 사용할 수 있습니다.

젠킨스 연동의 장점

CI 파이프라인에는 종종 다음과 같은 민감 정보가 필요하기 마련입니다:

  • GitHub Token
  • AWS Access Key / Secret
  • Docker Registry 인증 정보
  • DB 접속 비밀번호

👉 이런 정보를 Jenkins 내부에 하드코딩하거나 Jenkins Credentials에 직접 저장하면 노출 위험이 커지기 때문에 Vault와 연동하면 중앙에서 시크릿을 안전하게 관리하고, 필요할 때만 꺼내쓸 수 있게 됩니다.

 

CI/CD 보안 고려사항

이러한 민감정보의 관리에 있어서 vault와 CI툴의 연동은 매우 효율적입니다.

젠킨스와 Vault의 연동 워크플로우

젠킨스는 Vault에 시크릿으로 분류된 데이터를 필요로 하는 작업(job)을 실행해야 합니다.
젠킨스는 마스터 노드와 워커 노드를 가지고 있으며, 워커 노드는 짧은 시간 동안 실행되는 컨테이너 러너에서 작업을 실행합니다.

프로세스는 다음과 같습니다:

  1. 젠킨스 워커가 Vault에 인증
  2. Vault는 토큰을 반환
  3. 워커는 이 토큰을 사용해 작업에 해당하는 역할의 Wrapped SecretID를 요청
  4. Vault는 Wrapped SecretID를 반환
  5. 워커는 작업 러너를 생성하고, Wrapped SecretID를 변수로 전달
  6. 러너 컨테이너는 Wrapped SecretID의 unwrap을 요청
  7. Vault는 SecretID를 반환
  8. 러너는 RoleID와 SecretID를 사용해 Vault에 인증
  9. Vault는 필요한 시크릿 정보를 읽을 수 있는 정책이 포함된 토큰을 반환
  10. 러너는 이 토큰을 사용해 Vault에서 시크릿을 가져옴

이러한 과정을 실습을 통해 살펴보도록 하겠습니다.

1️⃣ Step1. Jenkins에서 Vault Plugin 설치

  1. Jenkins UI 접속
  2. 상단 메뉴에서 Manage JenkinsPlugins
  3. Available 탭에서 Vault 검색
  4. HashiCorp Vault Plugin 설치 후 Jenkins 재시작

2️⃣ Step2. Jenkins에서 Vault 설정 및 Credentials 추가

  1. Jenkins UI(admi/qwe123) → Manage JenkinsConfigure System

 

 

2. 스크롤 하단의 Vault Plugin Configuration 섹션으로 이동

Vault URL 입력 후 [Add] 버튼 클릭 : IP를 입력합니다.

3.Vault Credential 다음 값 입력:

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

해당 credential은 일전에 생성한 Role id와 secret id를 조회하여 집어넣습니다.

3️⃣ Step3. Jenkins Pipeline Job 생성

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

  1. 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}"
          }
        }
      }
    }
  }
}

파이프라인 스크립트를 작성합니다. 이때 본인 서버의 IP로 대입하여 작성합니다.

파이프라인을 실행해서 output log를 살펴보면 vault의 secret값이 로드된걸 확인 가능합니다. 이때 보안을 위해서 실제 값은 마스킹 처리가 됩니다.

Vault + ArgoCD Plugin 패턴 - 링크1 링크2

  • Argo CD에는 다양한 시크릿 관리 도구(HashiCorp Vault, IBM Cloud Secrets Manager, AWS Secrets Manager 등)플러그인을 통해 Kubernetes 리소스에 주입할 수 있도록 지원합니다.
  • 플러그인을 통해 Operator 또는 CRD(Custom Resource Definition)에 의존하지 않고 GitOps와 Argo CD로 시크릿 관리 문제를 해결할 수 있습니다.
  • 특히 Secret 뿐만 아니라, deployment, configMap 또는 기타 Kubernetes 리소스에도 사용할 수 있습니다.

Argo CD에서 vault를 사용하는 방법에는 네가지 방법이 있습니다. Docs 그 중에서도 다음 두 가지 방법을 일반적으로 많이 사용합니다.

  1. argocd-cm ConfigMap을 통한 설치:
    • argocd-repo-server에 InitContainer를 추가하여 플러그인을 다운로드하고 설정합니다.
    • 또는 플러그인이 사전 설치된 커스텀 이미지를 생성하여 사용합니다.
  2. 사이드카 컨테이너를 통한 설치:
    • 사이드카 컨테이너를 추가하여 플러그인과 필요한 도구들을 포함시킵니다.
    • 또는 플러그인이 사전 설치된 커스텀 사이드카 이미지를 생성하여 사용합니다.

이 중 사이드카 컨테이너를 활용한 방법은 Argo CD v2.4.0부터 도입된 최신 방식으로, 보안성과 유지보수 측면에서 권장됩니다.

따라서 이번 실습에서는 사이드카 컨테이너를 사용한 방법으로 진행해 봅니다.

1️⃣ Step 1. ArgoCD Vault Plugin을 위한 Credentials 활성화 - AppRole 인증

approle 인증을 위해 role_id와 secret_id를 k8s secret 리소스로 생성합니다.

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: your role id
  AVP_SECRET_ID: your secret id
EOF
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: 0df2b430-0e97-455c-8115-15f410d4c560
  AVP_SECRET_ID: cca72e88-ffb8-96ff-05ec-d3be4f480ddb
EOF

2️⃣ Step 2. ArgoCD Vault Plugin 설치

argocd에 vault 플러그인을 설치합니다.

git clone https://github.com/hyungwook0221/argocd-vault-plugin.git
cd argocd-vault-plugin/manifests/cmp-sidecar

 

해당 경로에는 다음과 같은 kustomize manifest 파일이 포함되어있습니다.

해당 kustomize로 정의된 리소스를 살펴보면 다음과 같습니다.

  1. ConfigMap (cmp-plugin)
    • argocd-vault-plugin을 사용하는 Config Management Plugin(CMP) 설정이 3개 포함됨:
      • avp-helm.yaml: Helm 템플릿 + Vault 시크릿 처리
      • avp-kustomize.yaml: Kustomize 빌드 + Vault 시크릿 처리
      • avp.yaml: YAML 전체를 Vault 필터링 처리
    • Argo CD와 Vault 연동에 사용되는 plugin 정의가 핵심
  2. Deployment (argocd-repo-server)
    • Argo CD의 repo-server 구성
    • 세 개의 CMP 컨테이너(avp-helm, avp-kustomize, avp)가 사이드카로 함께 실행됨
    • 각 컨테이너는 Vault 플러그인을 사용해서 템플릿을 처리
    • InitContainer(download-tools)를 통해 argocd-vault-plugin 바이너리를 다운로드하고 공유 볼륨에 설치함
    • cmp-plugin ConfigMap과 Vault Secret 마운트하여 CMP 동작을 구성

리소스를 생성합니다.

kubectl apply -n argocd -k .  

k exec -it -n vault vault-0 -- sh

# vault pod shell에 접속 후 root token으로 로그인
vault login
Token (will be hidden): <토큰입력>

# 확인명령
vault read auth/kubernetes/role/argocd

3️⃣ Step 3. 샘플 Application 배포하여 Vault와 동기화

vault를 사용할 샘플 애플리케이션을 배포합니다. GitHub에 저장된 Helm Repo을 배포하며, Helm 메니페스트 내에 변수로 치환된 값(username/password)을 CD 단계에서 Vault 통해서 읽고 렌더링하여 배포합니다.

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

아래는 Application 배포시 참조하는 new-values.yaml 입니다. (GitHub)

envs: 변수에 시크릿 값이 하드코딩이 아닌 vault를 통해 불러오도록 설정되어 있는 것을 확인할 수 있습니다.

serviceAccount:
  create: true

image:
  repository: luafanti/spring-boot-debug-app
  tag: main
  pullPolicy: IfNotPresent

replicaCount: 1

resources:
  memoryRequest: 256Mi
  memoryLimit: 512Mi
  cpuRequest: 500m
  cpuLimit: 1

probes:
  liveness:
    initialDelaySeconds: 15
    path: /actuator/health/liveness
    failureThreshold: 3
    successThreshold: 1
    timeoutSeconds: 3
    periodSeconds: 5
  readiness:
    initialDelaySeconds: 15
    path: /actuator/health/readiness
    failureThreshold: 3
    successThreshold: 1
    timeoutSeconds: 3
    periodSeconds: 5

ports:
  http:
    name: http
    value: 8080
  management:
    name: management
    value: 8081

envs:
  - name: VAULT_SECRET_USER
    value: <path:secret/data/sampleapp/config#username>
  - name: VAULT_SECRET_PASSWORD
    value: <path:secret/data/sampleapp/config#password>

log:
  level:
    spring: "info"
    service: "info"

3️⃣ Step 3-3) 실제 배포시 적용된 화면

argo 서버에서 배포 애플리케이션을 확인해보고 실제 배포된 화면도 살펴봅니다.

ArgoCD Vault Plugin 적용된 화면 : [DETAILS] - [PARAMETERS]

파라미터에 new-vaules.yaml 파일의 변수가 지정된 것을 확인할 수 있습니다.

 

Application 배포화면

 

argo cd에서 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)

VSO란?

Vault Secrets Operator(이하 “VSO”)를 사용하면, 애플리케이션이 Vault에 직접 접근하지 않아도, Kubernetes의 Secret을 통해 Vault의 시크릿 정보를 사용할 수 있게 됩니다.

VSO는 사전에 정의된 CRD(Custom Resource Definitions)의 변경을 지속적으로 감시하며 동작합니다.

Vault에서 시크릿이 변경되면, 해당 내용이 Kubernetes Secret에도 실시간으로 반영되도록 자동으로 동기화합니다.

덕분에 애플리케이션은 Kubernetes Secret만 읽으면 되고, Vault와의 직접 통신이나 복잡한 인증 처리가 필요 없어집니다.

즉, 시크릿 관리를 보다 간편하고 일관되게 할 수 있게 도와주는 도구입니다.

이번 실습은 VSO을 통해서 Vault을 통해 획득한 시크릿을 Kubernetes Secret을 자동으로 동기화하고 관리하는 방법을 알아보도록합니다.

역할 별 VSO 사용 사례

  • 운영자는 VaultConnection, VaultAuth CR을 생성하여 VSO가 Vault와 통신할 수 있도록 설정
  • VSO(SecretsOperator) 는 이 설정을 기반으로 Vault에 인증하고 토큰 발급 받음
  • 개발자가 생성한 VaultStaticSecret 등의 CR을 감시
  • VSO는 Vault에서 시크릿을 가져와 Kubernetes Secret으로 변환
  • 이 Secret은 애플리케이션 Pod에 자동 마운트됨 (/etc/secret)
  • 앱은 Vault를 몰라도 파일 시스템 경로로 시크릿을 안전하게 사용 가능

VSO 실습

VSO를 생성하여 진행할 실습 시나리오는 다음과 같습니다.

  • Spring(Web Application) → DB 접근하기 위해서는 DB Credentials을 K8s Secrets으로 참조해야합니다.
  • 대상 DB에 접근하기 위한 DB Credentials을 Vault의 Dynamic Secrets 기능을 활용하여 주기적으로 변경하고 VSO을 통해 K8s Secets에 갱신합니다.(업데이트)
  • Spring(Web Application)에서는 갱신된 DB Credentials 정보를 K8s Secret을 통해서 읽어오기 위해 재기동(rolloutRestartTargets 설정)

1️⃣ VSO Chart Values 파일 작성

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

Vault에 접근하기 위한 Kubernetes 인증 설정도 포함되어 있습니다. 여기에는 Vault에서 사전에 등록된 Role 이름(auth-role-operator), VSO가 사용하는 서비스 계정(vault-secrets-operator-controller-manager), 그리고 토큰의 audience 값(vault)이 정의되어 있어, Vault가 JWT 토큰을 신뢰하고 인증할 수 있도록 합니다.

 

2️⃣ VSO 배포

위에서 작성한 Value를 사용하여 helm 차트를 배포합니다.

helm install vault-secrets-operator hashicorp/vault-secrets-operator \
  -n vault-secrets-operator-system \
  --create-namespace \
  --values vault-operator-values.yaml

 

helm으로 생성된 k8s 리소스를 확인해 봅니다.

helm template vso hashicorp/vault-secrets-operator --values vault-operator-values.yaml

 

Vault Secrets Operator에서 정의한 VaultAuth 커스텀 리소스를 조회합니다.

kubectl get vaultauth -n vault-secrets-operator-system vault-secrets-operator-default-transit-auth -o jsonpath='{.spec}' | jq

  • method: kubernetes
  • → Vault의 Kubernetes 인증 방식을 사용
  • mount: k8s-auth-mount
  • → Vault에 설정된 Kubernetes Auth의 마운트 경로
  • kubernetes 정보:
    • role: auth-role-operator: Vault에 미리 정의된 Role 이름
    • serviceAccount: vault-secrets-operator-controller-manager: 이 서비스 계정이 Vault에 인증됨
    • audiences: ["vault"]: JWT 토큰의 audience 설정
    • tokenExpirationSeconds: 600: 발급된 토큰의 만료 시간(초 단위)
  • storageEncryption:
    • keyName: vso-client-cache: Vault의 Transit 엔진에 있는 암호화 키 이름
    • mount: demo-transit: Transit 엔진이 마운트된 경로
  • vaultConnectionRef: “default”
  • → 연결에 사용할 VaultConnection 리소스 이름

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

 

4️⃣ Kubernetes Auth Method 설정

쿠버네티스 API 통신을 위해 정보를 구성, Vault VSO 인증 메서드를 활성화 해줍니다.

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

# 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

 

5️⃣ Vault Kubernetes Auth Role 생성

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은 Kubernetes 서비스 계정이 Vault에 인증 요청을 보내면, 어떤 정책과 토큰 조건으로 인증을 허용할지”를 정의합니다.

 

6️⃣ Vault Database Secret Engine 설정

DB 연동에 필요한 인증 정보 및 엔드포인트 설정 등을 vault로 관리하기 위한 시크릿과 엔진 설정을 하여줍니다.

#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분)
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️⃣ Vault Transit Secret Engine 설정 및 VSO 캐시 암호화 구성

캐싱시 암호화 처리를 위해 필수적으로 구현해야합니다.

kubectl exec --stdin=true --tty=true vault-0 -n vault -- /bin/sh

# Transit Secret Engine 활성화
# transit 엔진을 demo-transit 경로로 활성화.
#   - 데이터를 저장하지 않고 암복호화 기능만 제공하는 Vault의 기능
vault secrets enable -path=demo-transit transit

# 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️⃣ 샘플 애플리케이션 구성 및 배포

demo-ns 네임스페이스 생성

kubectl create ns demo-ns
mkdir vso-dynamic
cd vso-dynamic

 

vault-auth-dynamic.yaml

  • 앱이 Vault에 인증하기 위한 ServiceAccount 및 VaultAuth 리소스
  • Vault에서 발급한 동적 PostgreSQL 크레덴셜을 얻기 위해 반드시 필요
cat <<EOF | kubectl apply -f -
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
EOF

 

시크릿 생성 - spring app

  • Spring App에서 PostgreSQL에 접속할 때 사용할 해당 시크릿에 username/password을 동적으로 생성
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
  name: vso-db-demo
  namespace: demo-ns
EOF

 

시크릿 생성 - Postgre

  • Vault에서 동적으로 PostgreSQL 접속정보 생성하고 K8s Secret에 저장
  • 생성된 Secret(vso-db-demo)은 앱에서 환경 변수(env)로 사용
  • 애플리케이션에서 Dynamic Reloading을 지원하지 않을 경우 rolloutRestartTargets 을 사용하여 애플리케이션을 재배포하여 새로 업데이트된 시크릿을 사용하도록 할 수 있음
  • refreshAfter 설정을 통해 VSO가 소스 시크릿 데이터를 동기화 하는 주기 설정가능 - Docs
    → Event Notification 기능을 통해 변경이 있을 경우 즉시 반영할 수도 있음 - Docs
cat <<EOF | kubectl apply -f -
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**
EOF

 

애플리케이션 배포

  • DB 접속 테스트를 위한 Spring App
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vaultdemo
  namespace: demo-ns
  labels:
    app: vaultdemo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vaultdemo
  template:
    metadata:
      labels:
        app: vaultdemo
    spec:
      volumes:
        - name: secrets
          secret:
            secretName: "vso-db-demo"
      containers:
        - name: vaultdemo
          image: hyungwookhub/vso-spring-demo:v5
          imagePullPolicy: IfNotPresent
          **env:
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: "vso-db-demo"
                  key: password
            - name: DB_USERNAME
              valueFrom:
                secretKeyRef:
                  name: "vso-db-demo"
                  key: username**
            - name: DB_HOST
              value: "postgres-postgresql.postgres.svc.cluster.local"
            - name: DB_PORT
              value: "5432"
            - name: DB_NAME
              value: "postgres"
          ports:
            - containerPort: 8088
          volumeMounts:
            - name: secrets
              mountPath: /etc/secrets
              readOnly: true
---
apiVersion: v1
kind: Service
metadata:
  name: vaultdemo
  namespace: demo-ns
spec:
  ports:
    - name: vaultdemo
      port: 8088         
      targetPort: 8088 
      nodePort: 30003
  selector:
    app: vaultdemo
  type: NodePort
EOF

 

9️⃣ 동작확인(UI) : postgres-postgresql.postgres.svc , db는 postgres

  • localhost:30003 접속 후 K8s Secrets에 생성된 username, password 값으로 연결 테스트합니다.

 

파드가 재기동되면 username과 password가 실시간으로 바뀝니다.

 

9️⃣ 동작확인(CLI)

다음과 같은 일련의 명령어를 통해 시크릿이 관리되는 것을 확인해봅니다.

# 스크릿이 변경되고, 적용을 위해서 파드가 42~45초 사이로 재생성됨
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-7779bcc8c6-tn74g   1/1     Running   0          27s

_raw='{"password":"igZwq-mC04usPQ7QOVDM","username":"v-k8s-auth-dev-post-5mYscZjhGgMhqcWIRpjo-1744273869"}'
password='igZwq-mC04usPQ7QOVDM'
username='v-k8s-auth-dev-post-5mYscZjhGgMhqcWIRpjo-1744273869'
Thu Apr 10 17:31:36 KST 2025

NAME                         READY   STATUS    RESTARTS   AGE
vaultdemo-59784754f8-d85bm   1/1     Running   0          12s

_raw='{"password":"V2-a1wJQk7MhDACwJYfb","username":"v-k8s-auth-dev-post-HYgtwKnrMqBML4831xiO-1744273914"}'
password='V2-a1wJQk7MhDACwJYfb'
username='v-k8s-auth-dev-post-HYgtwKnrMqBML4831xiO-1744273914'
Thu Apr 10 17:32:06 KST 2025

NAME                         READY   STATUS              RESTARTS   AGE
vaultdemo-59784754f8-d85bm   1/1     Running             0          42s
vaultdemo-79576986c7-cb5h9   0/1     ContainerCreating   0          0s

_raw='{"password":"-YZ2ebxyEskpl1ZP1Mcm","username":"v-k8s-auth-dev-post-LGpcqVAfuYdWNNWBkbKA-1744273956"}'
password='-YZ2ebxyEskpl1ZP1Mcm'
username='v-k8s-auth-dev-post-LGpcqVAfuYdWNNWBkbKA-1744273956'
Thu Apr 10 17:32:36 KST 2025

# 실제 postgresql 에 사용자 정보 확인 : 계속 추가되고 있음..
kubectl exec -it -n postgres postgres-postgresql-0 -- sh -c "PGPASSWORD=secret-pass psql -U postgres -h localhost -c '\du'"
                                                  List of roles
                      Role name                      |                         Attributes                         
-----------------------------------------------------+------------------------------------------------------------
 postgres                                            | Superuser, Create role, Create DB, Replication, Bypass RLS
 v-k8s-auth-dev-post-23yTKlIlbnInwyQAaO55-1744274290 | Password valid until 2025-04-10 08:39:15+00
 v-k8s-auth-dev-post-3Vtbg8lmlyxRRzHfM7Zt-1744272836 | Password valid until 2025-04-10 08:15:01+00
 v-k8s-auth-dev-post-5I5y27xxG61G1m1MxPWp-1744273652 | Password valid until 2025-04-10 08:28:37+00
 v-k8s-auth-dev-post-5mYscZjhGgMhqcWIRpjo-1744273869 | Password valid until 2025-04-10 08:32:14+00
 v-k8s-auth-dev-post-6x0KoTXiUn0XcattYd5A-1744273218 | Password valid until 2025-04-10 08:21:23+00
 v-k8s-auth-dev-post-7EzuV4eRnE3sRLaGsM2h-1744273475 | Password valid until 2025-04-10 08:25:40+00
 v-k8s-auth-dev-post-8SRaQap9V1Rcd4ZQ9k73-1744273607 | Password valid until 2025-04-10 08:27:52+00
 v-k8s-auth-dev-post-9AHIb55XgqeDFqwphmby-1744274501 | Password valid until 2025-04-10 08:42:46+00
...

# 로그 확인
kubectl stern -n demo-ns -l app=vaultdemo
...

kubectl stern -n vault vault-0
...
vault-0 vault 2025-04-10T08:32:09.484Z [INFO]  expiration: revoked lease: lease_id=demo-db/creds/dev-postgres/Ph1sg8efnqssOU5FVIuAqhMW
vault-0 vault 2025-04-10T08:32:54.229Z [INFO]  expiration: revoked lease: lease_id=demo-db/creds/dev-postgres/XKkHZ65ZYLeILvo8GqWfE7F3
vault-0 vault 2025-04-10T08:33:36.804Z [INFO]  expiration: revoked lease: lease_id=demo-db/creds/dev-postgres/rk1KUFsV7SjZPNFsCqCHGZVx
...

# vault-secrets-operator 가 clientCachePersistenceModel 설정 적용 정보 확인 : 참고로 해당 파드가 재시작되지는 않음.
kubectl stern -n vault-secrets-operator-system -l app.kubernetes.io/name=vault-secrets-operator
...
vault-secrets-operator-controller-manager-7f67cd89fd-d2t2k manager {"level":"info","ts":"2025-04-10T06:50:14Z","logger":"initCachingClientFactory","msg":"Initializing the CachingClientFactory"}
vault-secrets-operator-controller-manager-7f67cd89fd-d2t2k manager {"level":"info","ts":"2025-04-10T06:50:14Z","logger":"setup","msg":"Starting manager","gitVersion":"0.10.0","gitCommit":"aebf0c1c59485059a9ea6c58340fd406afe4cbef","gitTreeState":"clean","buildDate":"2025-03-04T22:22:24+0000","goVersion":"go1.23.6","platform":"linux/arm64","clientCachePersistenceModel":"direct-encrypted","clientCacheSize":10000,"backoffMultiplier":1.5,"backoffMaxInterval":60,"backoffMaxElapsedTime":0,"backoffInitialInterval":5,"backoffRandomizationFactor":0.5,"globalTransformationOptions":"","globalVaultAuthOptions":"allow-default-globals"}
...
vault-secrets-operator-controller-manager-7f67cd89fd-d2t2k manager {"level":"info","ts":"2025-04-10T08:11:00Z","logger":"lifetimeWatcher","msg":"Starting","id":"aee67272-40e9-41ba-a380-b9f948acea7e","entityID":"77fb779f-c79e-567e-1c72-e83c0a0552a7","clientID":"024b5ed8d389816c9a4a99f2968ffc4ba66282d7f39d34b465aba1ebe3a8bb67","cacheKey":"kubernetes-5530fb1481fb1695773196"}
...

# 암호화된 캐시 저장소 확인 : vso-cc-<auth method>
kubectl get secret -n vault-secrets-operator-system -l app.kubernetes.io/component=client-cache-storage 
NAME                                       TYPE     DATA   AGE
vso-cc-kubernetes-5530fb1481fb1695773196   Opaque   2      5m21s

kubectl get secret -n vault-secrets-operator-system -l app.kubernetes.io/component=client-cache-storage -o yaml
...
  data:
    messageMAC: AZ2WpecD1Ecc4fu6TQY8qtqrIGuZEBfBIIecuOrLMgY=
    secret: eyJjaXBoZXJ0ZXh0IjoidmF1bHQ6djE6c1U3RkVjZ2NldW1SSDR4K3hjRGF1eTNTRzNyejBnMUgxZlY4TmozRUJ5cCtuZnN6WWxMTmwrSDdtVy96ZnAvcVNUVDMxeU5ORlNTTjNWOFh6Zkx6TlJnMGt3QXZaeTBwbUZXclFlUGVBVmdLU3RSSllDSVA0YjRkTnk1cWgxcFA4Tnh0cDVvV1QvRnRpNWptZWpBN2FxWkxXRjJ3L1Y1K2FLZ0tTN1hZcG9pRnJpUTc3QWRvaEltTjlweUdpVHZVNFF4OTVSOW94Y3N6cW1JaTB6MUVCZ2l4U1AxWmNqUDZkSUN0QTREK3hTMWsxNmtneFV2elpUZHN5R01QS01manBzVGRCbmQ1NnFJYlQ1OTZ4bEh3WmJvSnU4dUhaY1pKaGVtWnc2eUJXekV4VHdYTXIxV3ZqbVdZWXNwL0dkTzRXTktpZE5nQjB3QnBqM0o3VFd5UEgreWVJTXFkREdmZWFCWkxhRkJQN1VtNnNpNEI1TUZSRFQrdElEZVhER3dLcjdCbWRSMVVpT0M5WU9hN1MxRlpacGFGTis2QXJRUUJDMGFicHZNSlN1blQ0R1o4L1lGUUpPcnFYaTlwNWJrYUhWUFU5S1dXQ2VtTFRqRDJHdnN5ZkZkMmkyZlhJNHR5Ly8vczZIYndGbDZZT0EyRWw1c1QxbjNkbkF6NXFXOUtuVUdYYVVTY0NhWTRDOG5rU3dLeVlRejY2WHU2QkJvYXkvT3k0QjRBUy83bS9seXpHRCtQZUFNQU84cjE1YmlUZTgvdWtmZWIxTHl1cXVFbG5saGdHMmxYTjFSd1laSElwVnpuZXlkZWlYd2RsdW5leFVXeFk3QUg2QjNEVmVTWWlWczE5dEIxRGt2Mm1KbkJLRThtL09QcVliSENYaS9zTXFEMGpoYjFQUWJ4QzdiQkY3ODRIQnByUDFDNmo0Szd5VVpKdGtqVUpsVTBxclRpMXBNQS9nbnovVXV2RHZSb3RUTkE5RTk1U0I1c1pVbmZsZHlsZGNCNjZ3RG5oaXJDNkluZnlmcFN2ZUFMNTErNUc3bDZzOXlVbUQ4aDNpQW5nWTlEemt1bGlmakxkYyt4VE9sY2ZjUWp2RVJ1UVA0b2psTkxvY3BuOVZ4b3E4ZUZDZllsaEVENlNJTzZCdHc4SWZWWndZa0JNWHVieVFtMGl1b3VXaVFZcXdnNEhSL2l1SVZyMVB2S3ZZcUJ3LzhHQ1g4QjhyVzM0SWFHeHlIRGN4UG9kUHJuKy95QnFYSVphMm1IZDBkempOQjB4UytVYU5WZS9GamdLM1hnbE1aSkdjWldyT0NxbDQrYmpCa1NJTkVXUHV5Y3BoL1hxRFVmVStMTDJFTEsxRUo5OUprTUd3ZEdzRDBOR0FjZTIrclpPQTVNNUk4aDE4R040aEt1MUdZPSIsImtleV92ZXJzaW9uIjoxfQ==
  immutable: true
...

 

마치며

실습 자원 삭제

kind delete cluster --name myk8s
docker compose down --volumes --rmi all

 

이번 포스팅에서는 Kubernetes 환경에서 HashiCorp Vault를 활용한 시크릿 관리 전반을 실습을 통해 체험해보았습니다. 단순한 정적 시크릿 저장부터 시작해, AppRole 인증, Vault Agent Sidecar 패턴, 그리고 Vault Secrets Operator(VSO)를 통한 동적 시크릿(Dynamic Secret) 관리까지 실제 서비스 환경에 가까운 구성을 따라가며 Vault의 강력한 기능을 익힐 수 있었습니다.

Vault는 단순한 시크릿 저장소를 넘어서, 클라우드 네이티브 환경에서 필수적인 보안 컴포넌트로 자리 잡고 있습니다. 앞으로 운영 환경에서 민감 정보를 다룰 때, Vault를 어떻게 활용할 수 있을지 고민하고 적용해본다면 보다 안전하고, 자동화된 인프라 운영이 가능해질 것이라 생각합니다.

'AWS > EKS' 카테고리의 다른 글

GenAI on EKS with inferentia & FSx Workshop  (1) 2025.04.19
EKS Upgrades  (0) 2025.04.06
K8S 환경에서 CI/CD 구축하기 - 2  (0) 2025.03.30
K8S 환경에서 CI/CD 구축하기 - 1  (0) 2025.03.30
EKS Auto Mode  (0) 2025.03.23