Ssoon

HashiCorp Vault - Vault Agent와 Sidecar 패턴 본문

CICD Study [1기]

HashiCorp Vault - Vault Agent와 Sidecar 패턴

구구달스 2025. 11. 27. 20:38

출처 : https://www.redhat.com/en/blog/integrating-hashicorp-vault-in-openshift-4

🧰 Vault Agent + Sidecar 패턴이란 무엇인가?

  • Vault Agent
    Vault Agent는 Vault와 상호작용하며, 인증(authentication), 시크릿(secret) 조회 및 갱신, 템플릿 렌더링(template rendering) 등을 자동화해주는 경량 클라이언트 프로그램입니다. Vault 서버를 직접 호출하는 대신 Vault Agent를 사용하면 애플리케이션이 Vault의 내부 동작을 알 필요 없이 시크릿을 안전하게 취득할 수 있습니다.
  • Sidecar 패턴
    Sidecar 패턴은 Kubernetes에서 자주 사용되는 설계 방식으로, 애플리케이션 컨테이너와 동일한 Pod 내에 “추가적인 보조 컨테이너”를 둠으로써, 로그 수집, 보안, 프록시, 설정 관리 등의 기능을 애플리케이션 코드 변경 없이 제공하는 방식입니다. Vault Agent를 sidecar로 두면, 애플리케이션은 Vault에 직접 접근하지 않고도 secret을 사용할 수 있습니다.

"Vault Agent + Sidecar 패턴은 애플리케이션이 Vault의 존재나 인증 로직을 몰라도 시크릿을 안전하게 주입받을 수 있게 해준다."


✅ 왜 Vault Agent + Sidecar 방식을 선택하는가?

  • 애플리케이션 코드 변경 불필요
    애플리케이션이 Vault SDK를 사용하거나 Vault 호출 로직을 갖출 필요 없이, 단순히 환경 변수나 파일로 시크릿을 공급받을 수 있습니다. 따라서 개발 또는 배포 복잡도를 줄일 수 있습니다. 
  • 자동 인증 및 시크릿 갱신/회전 지원
    Vault Agent는 Kubernetes 인증을 사용해 자동으로 로그인하고 토큰을 받아올 수 있습니다. 또한 시크릿이 만료되거나 rotate가 필요한 경우, 에이전트가 자동으로 처리해줄 수 있습니다. 
  • Pod 내부에 안전한 시크릿 전달
    조회된 시크릿은 Pod 내 인메모리(tmpfs) 볼륨이나 임시 파일 시스템에 렌더링되어 저장됩니다. 이 방식은 시크릿을 환경 변수 형태로 제공하거나 파일로 주입할 때, Vault나 인증 정보가 애플리케이션에 노출되지 않도록 도와줍니다. 
  • Vault-native 기능 활용 가능
    Vault Agent는 단순 조회 뿐 아니라 templating, caching, renewal 등 Vault-native한 기능을 활용합니다. CSI 방식 등 다른 통합 방식에서 제공되기 어려운 유연성을 제공합니다. 

"Vault Agent sidecar는 단순 secret 조회를 넘어, 자동 인증·갱신·템플릿 기능을 제공하는 Vault의 ‘클라이언트 사이드 확장’이다." 


🔄 Vault Agent + Sidecar의 동작 흐름 (Kubernetes 관점)

  1. Pod 생성 시 Sidecar 주입 (Injection)
    • Kubernetes에 특정 annotation(e.g. vault.hashicorp.com/agent-inject: "true")을 달아두면, Vault Agent Injector는 Mutating Webhook을 통해 해당 Pod 스펙에 Vault Agent 컨테이너(sidecar)와 init-container를 자동으로 삽입합니다. 
    • 이 과정을 통해 애플리케이션 개발자는 별도 설정 없이 Vault 통합을 적용할 수 있습니다.
  2. Vault 인증 및 시크릿 조회 (Init / Agent)
    • init-container 또는 Agent 컨테이너는 Kubernetes ServiceAccount의 토큰을 사용해 Vault의 Kubernetes auth method로 Vault에 로그인합니다.
    • 유효한 토큰이 확인되면, 필요한 secret path를 Vault에 조회하고, 데이터를 가져옵니다.
  3. 시크릿 렌더링 및 제공
    • Vault Agent는 조회한 시크릿을 Pod 내의 메모리 볼륨(tmpfs) 또는 임시 파일 시스템에 파일 형태로 렌더링합니다 (예: /vault/secrets/...). 
    • 애플리케이션 컨테이너는 이 파일이나 환경 변수에서 시크릿을 읽어 사용합니다. 애플리케이션은 Vault 존재 여부를 알 필요가 없습니다. 
  4. 자동 갱신 및 시크릿 회전 (선택 사항)
    • Vault Agent는 토큰이나 시크릿이 만료될 경우 자동으로 갱신하거나, 시크릿 rotation 정책이 있는 경우 주기적으로 갱신 작업을 수행할 수 있습니다.

"Pod 생성 → Vault 인증 → 시크릿 조회 → 시크릿 파일/환경변수 렌더링 → 애플리케이션 사용" 이 흐름이 Vault Agent Sidecar의 기본 동작이다. 


⚠️ 주의사항 & 한계점

  • Kubernetes 버전 및 Vault Agent Injector 호환성
    특정 Kubernetes 버전 (예: 1.21+)에서는 ServiceAccount 토큰 방식이나 JWT 토큰 방식의 변경으로 인해 Vault Injector가 정상 동작하지 않을 수 있다는 보고가 있습니다. 버전 호환성 확인이 필요합니다. 
  • 복잡한 Pod 구성 시 Sidecar 충돌 가능성
    여러 sidecar (예: logging, monitoring, Istio, Vault 등)를 동시에 사용하는 경우, 리소스 충돌이나 initialization 순서 문제, 볼륨 마운트 충돌 등이 발생할 수 있습니다. Sidecar 패턴의 복잡성이 늘어날수록 관리 복잡도도 커집니다.
  • 시크릿 렌더링 위치와 보안 고려
    시크릿이 Pod 내 메모리 또는 파일에 렌더링되므로, Pod가 탈취되거나 로그가 남으면 시크릿이 유출될 위험이 있습니다. 따라서 Pod 보안, RBAC, 네트워크 정책, Vault 정책 설계 등을 신중히 해야 합니다.
  • Vault Agent Injector 사용의 운영 오버헤드
    모든 Pod에 sidecar를 주입하면 리소스 오버헤드가 발생할 수 있고, injector 설정과 annotation 관리가 필요합니다. 단순한 환경에서는 오히려 과한 구조일 수 있습니다.

"Vault Agent Sidecar는 강력하지만, Kubernetes 환경과 운영 방식에 따라 복잡성과 보안 리스크를 함께 고려해야 한다."


 Vault AppRole 방식 인증 구성 

  • AppRole 인증 방식 활성
    • vault auth list
      • Vault에 현재 활성화된 인증 방법(auth method) 목록을 보여줍니다.  
    •  vault auth enable approle
      • Vault에 AppRole 인증 방식을 추가합니다.
      • AppRole은 애플리케이션이나 서비스가 Vault에 안전하게 로그인할 때 사용하는 방법입니다.
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ vault auth list
Path      Type     Accessor               Description                Version
----      ----     --------               -----------                -------
token/    token    auth_token_c549d10b    token based credentials    n/a

(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ vault auth enable approle || echo "AppRole already enabled"
Success! Enabled approle auth method at: approle/

(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ vault auth list
Path        Type       Accessor                 Description                Version
----        ----       --------                 -----------                -------
approle/    approle    auth_approle_61ea9ba0    n/a                        n/a
token/      token      auth_token_c549d10b      token based credentials    n/a
  • token/ → 기존 토큰 인증
  • approle/ → 새로 추가된 AppRole 인증

  • 정책 생성
    • vault policy write → 새로운 정책을 Vault에 등록하는 명령어
    • sampleapp-policy → 정책 이름
    • path "secret/data/sampleapp/*" → 이 정책이 적용되는 경로 (여기서는 secret/data/sampleapp/ 아래 모든 키)
    • capabilities = ["read"] → 이 경로에 대해 읽기 권한만 허용
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ vault policy write sampleapp-policy - <<EOF
path "secret/data/sampleapp/*" {
  capabilities = ["read"]
}
EOF
Success! Uploaded policy: sampleapp-policy
  • Vault는 정책 기반 접근 제어를 사용합니다.
  • AppRole을 만들 때 이 정책을 연결하면, 해당 AppRole로 로그인한 애플리케이션은 secret/data/sampleapp/* 경로의 데이터를 읽기만 가능하게 됩니다

  • AppRole Role 생성 - 앞서 생성한 정책(sampleapp-policy) 연결
    • auth/approle/role/sampleapp-role  sampleapp-role이라는 AppRole을 생성
    • token_policies="sampleapp-policy" → 이 AppRole로 발급되는 토큰은 sampleapp-policy 정책을 따름
    • secret_id_ttl="1h" → Secret ID(로그인용 비밀키)의 유효기간은 1시간
    • token_ttl="1h" → 발급된 토큰 기본 유효기간은 1시간
    • token_max_ttl="4h" → 토큰 최대 연장 가능 시간은 4시간
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ vault write auth/approle/role/sampleapp-role \
  token_policies="sampleapp-policy" \
  secret_id_ttl="1h" \
  token_ttl="1h" \
  token_max_ttl="4h"
Success! Data written to: auth/approle/role/sampleapp-role

  • Role ID 및 Secret ID 추출 및 저장
    • ROLE_ID
      AppRole을 식별하는 고유 ID입니다.
       sampleapp-role이라는 역할을 나타냄.
    • SECRET_ID
      AppRole로 로그인할 때 사용하는 비밀 키입니다.
      → 보안상 안전하게 전달해야 하며, TTL(유효기간)이 있습니다.
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ ROLE_ID=$(vault read -field=role_id auth/approle/role/sampleapp-role/role-id)
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ SECRET_ID=$(vault write -f -field=secret_id auth/approle/role/sampleapp-role/secret-id)
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ echo "ROLE_ID: $ROLE_ID"
ROLE_ID: 59bcf2b3-5ab2-58f8-a631-055b2039ea30
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ echo "SECRET_ID: $SECRET_ID"
SECRET_ID: 1e13ce12-f596-4050-307f-6525bc3ed51d

  • 파일로 저장
    • ROLE_ID와 SECRET_ID는 AppRole 인증에 필요한 값이고, 애플리케이션이나 Vault Agent가 이 값을 읽어서 Vault에 로그인할 수 있어야 합니다.
    • 파일로 저장하면 Kubernetes Secret이나 CI/CD 파이프라인에서 쉽게 참조할 수 있습니다.
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ mkdir -p approle-creds
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ echo "$ROLE_ID" > approle-creds/role_id.txt
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ echo "$SECRET_ID" > approle-creds/secret_id.txt

  • Kubernetes Secret으로 저장 (Agent 인증시 AppRole Role ID, Secret ID 사용)
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ 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 -
secret/vault-approle created

(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ k get secret -n vault
NAME                          TYPE                 DATA   AGE
sh.helm.release.v1.vault.v1   helm.sh/release.v1   1      132m
vault-approle                 Opaque               2      24s

Vault Agent Sidecar 연동

  • Vault Agent 설정 파일 작성 및 생성 (vault-agent-config.hcl) - HCL(HashiCorp Configuration Language)
    • Vault 주소 지정
      Vault Agent가 Vault 서버(vault.vault.svc:8200)에 연결하도록 설정했습니다.
    • AppRole 인증 자동화
      • ROLE_ID와 SECRET_ID를 지정된 경로에서 읽어 Vault에 로그인합니다.
      • 로그인 후 발급된 Vault 토큰을 파일로 저장합니다.
    • Secret 템플릿 렌더링
      • Vault에서 secret/data/sampleapp/config 경로의 데이터를 읽습니다.
      • username과 password 값을 HTML 템플릿에 넣어 /etc/secrets/index.html 파일로 생성합니다.
      • 20초마다 Secret을 다시 확인하고 템플릿을 갱신합니다.
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ 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
configmap/vault-agent-config created
  • 샘플 애플리케이션 + Sidecar 배포 (수동방식)
    • nginx 컨테이너: 웹 서버 역할. /usr/share/nginx/html 경로를 문서 루트로 사용합니다.
    • Vault Agent 사이드카:
      • 앞서 만든 설정(ConfigMap)을 읽어 AppRole로 자동 인증합니다.
      • Vault에서 Secret을 가져와 HTML 파일로 렌더링합니다.
      • 렌더링 결과를 nginx가 서빙하는 경로(공유 볼륨)에 저장해, 웹으로 바로 확인할 수 있게 합니다.
    • 볼륨 구성:
      • ConfigMap(Agent 설정), Secret(AppRole 자격증명), EmptyDir(토큰 저장, HTML 출력)을 컨테이너 간에 공유하도록 마운트
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ 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
deployment.apps/nginx-vault-demo created
  • SVC 생성
    • Service (NodePort)가 생성되어, 클러스터 외부에서 nginx 컨테이너의 80번 포트로 접근할 수 있습니다.
    • 셀렉터로 Deployment의 라벨(app: nginx-vault-demo)과 연결되어 Pod로 트래픽이 전달됩니다.
    • NodePort 30001을 열어 외부 접속을 허용합니다. (Kind 환경 기준)
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ 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
service/nginx-service created
  • 생성된 컨테이너 확인
    • nginx-vault-demo Pod가 2개의 컨테이너(nginx, Vault Agent) 로 정상 실행 중입니다.
    • Vault Agent 사이드카가 /etc/vault/agent-config.hcl 설정을 읽어 AppRole 인증을 수행하고 있습니다.
    • 볼륨 마운트도 정상적으로 되어 있어, Vault Agent가 Secret을 HTML로 렌더링하고 nginx가 이를 서빙할 준비가 완료되었습니다.
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ kubectl get pod -l app=nginx-vault-demo
NAME                                READY   STATUS    RESTARTS   AGE
nginx-vault-demo-7776649597-9dm4s   2/2     Running   0          52s
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ kubectl describe pod -l app=nginx-vault-demo
Name:             nginx-vault-demo-7776649597-9dm4s
...
Containers:
...
  vault-agent-sidecar:
    Container ID:  containerd://44b43d866eda2f6f1db12bd8e16fa89e39d2c4b50055e0c23e02b21cab65b0d5
    Image:         hashicorp/vault:latest
    Image ID:      docker.io/hashicorp/vault@sha256:f4e2687b72858a9e2160c344c9fa1ef74c07f21a89a8c00534ab64d3f187b927
    Port:          <none>
    Host Port:     <none>
    Args:
      agent
      -config=/etc/vault/agent-config.hcl
    State:          Running
      Started:      Sat, 29 Nov 2025 23:02:05 +0900
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /etc/secrets from html-volume (rw)
      /etc/vault from vault-agent-config (rw)
      /etc/vault-agent-token from vault-token (rw)
      /etc/vault/approle from vault-approle (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-262s9 (ro)
  • AppRole 인증 → Vault Agent → Secret 주입 → 애플리케이션 사용
    • Vault Agent 사이드카가 정상적으로 실행 중이며, 설정 파일(agent-config.hcl)도 올바르게 적용되었습니다.
    • AppRole 자격증명(role_id, secret_id)이 Pod 내부에서 확인되었고, Vault Agent가 이를 사용해 인증을 수행했습니다.
    • Vault에서 가져온 Secret(username, password)이 템플릿을 통해 HTML로 렌더링되어 nginx 컨테이너의 /usr/share/nginx/html/index.html에 저장되었습니다.
    • index.html 파일을 확인한 결과, Vault의 값(demo, p@ssw0rd)이 정상적으로 표시되고 있습니다.
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ kubectl get mutatingwebhookconfigurations.admissionregistration.k8s.io
NAME                       WEBHOOKS   AGE
vault-agent-injector-cfg   1          136m

(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ kubectl  exec -it -n vault deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/vault/agent-config.hcl
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
}
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ kubectl  exec -it -n vault deploy/nginx-vault-demo -c vault-agent-sidecar -- ls -l /etc/vault/approle
total 0
lrwxrwxrwx    1 root     root            14 Nov 29 14:01 role_id -> ..data/role_id
lrwxrwxrwx    1 root     root            16 Nov 29 14:01 secret_id -> ..data/secret_id

(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ kubectl  exec -it -n vault deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/vault/approle/role_id
59bcf2b3-5ab2-58f8-a631-055b2039ea30

(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ kubectl  exec -it -n vault deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/vault/approle/secret_id
1e13ce12-f596-4050-307f-6525bc3ed51d

(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ kubectl exec -it -n vault deploy/nginx-vault-demo -c nginx -- ls -l /usr/share/nginx/html
total 4
-rw-r--r-- 1 100 1000 94 Nov 29 14:02 index.html

(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ kubectl exec -it -n vault deploy/nginx-vault-demo -c nginx -- cat /usr/share/nginx/html/index.html
  <html>
  <body>
    <p>username: demo</p>
    <p>password: p@ssw0rd</p>
  </body>
  </html>

  • KV 값 변경 후 확인

  • Vault Agent가 설정한 주기적 렌더링(20초) 덕분에 Secret 변경 사항이 자동으로 업데이트
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ kubectl exec -it -n vault deploy/nginx-vault-demo -c nginx -- ls -l /usr/share/nginx/html
total 4
-rw-r--r-- 1 100 1000 98 Nov 29 14:08 index.html
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ kubectl exec -it -n vault deploy/nginx-vault-demo -c nginx -- cat /usr/share/nginx/html/index.html
  <html>
  <body>
    <p>username: demo</p>
    <p>password: new-p@ssw0rd</p>
  </body>
  </html>
  • 리소스 삭제
(⎈|kind-myk8s:vault) ssoon@DESKTOP-72C919S:~$ kubectl delete deployment/nginx-vault-demo \
                deployment/vault-injected-ui \
                service/nginx-service \
                service/vault-injected-ui \
                sa/vault-ui-sa \
                secret/vault-approle -n vault
deployment.apps "nginx-vault-demo" deleted from vault namespace
service "nginx-service" deleted from vault namespace
secret "vault-approle" deleted from vault namespace

 

 

 

 

 

Comments