가시다님이 진행하는 CI/CD 스터디 마지막 세션입니다.

vault를 kubernetes에서 다뤄보는 실습을 진행해보겠습니다.

vault on k8s

Quick install

클러스터 생성

kind create cluster --name myk8s --image kindest/node:v1.32.8 --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  labels:
    ingress-ready: true
  extraPortMappings:
  - containerPort: 30000  # Vault Web UI
    hostPort: 30000
  - containerPort: 30001  # Sample application
    hostPort: 30001
EOF

# 노드에 기본 툴 설치
docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget net-tools dnsutils tcpdump ngrep iputils-ping git vim -y'

vault 설치

helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update

helm install vault hashicorp/vault -n vault --create-namespace --version 0.31.0 \
  --set global.enabled=true \
  --set global.tlsDisable=true \
  --set server.standalone.enabled=true \
  --set-file server.standalone.config=<(cat <<'EOF'
ui = true
listener "tcp" {
  address = "[::]:8200"
  cluster_address = "[::]:8201"
  tls_disable = 1
}
storage "file" {
  path = "/vault/data"
}
EOF
) \
  --set server.dataStorage.enabled=true \
  --set server.dataStorage.size="10Gi" \
  --set server.dataStorage.mountPath="/vault/data" \
  --set server.auditStorage.enabled=true \
  --set server.auditStorage.size="10Gi" \
  --set server.auditStorage.mountPath="/vault/logs" \
  --set server.service.enabled=true \
  --set server.service.type=NodePort \
  --set server.service.nodePort=30000 \
  --set ui.enabled=true \
  --set injector.enabled=false
  
  # 리소스 배포확인
  kubectl krew install get-all
  kubectl get-all -n vault

valut의 Sealed 개념

Sealed(봉인) 상태는 Vault의 보안 메커니즘

Sealed 상태 Unsealed 상태

Vault가 암호화 키를 메모리에서 제거한 상태  
저장된 데이터는 접근 불가  
API 요청 대부분이 거부됨 (health check 등 일부만 허용) 암호화 키가 메모리에 로드된 상태
데이터 읽기/쓰기 가능  
정상 동작 상태  

valut status 명령으로 Sealed 상태확인

kubectl exec -ti vault-0 -n vault -- vault status
Key                Value
---                -----
Seal Type          shamir 
Initialized        false
Sealed             true # Sealed true로 확인

valut Unseal 수행

kubectl exec vault-0 -n vault -- vault operator init \
    -key-shares=1 \
    -key-threshold=1 \
    -format=json > cluster-keys.json

# key값 추출
jq -r ".unseal_keys_b64[]" cluster-keys.json
OcEZTfz6zMS/N1oYRzrj9tJwJHrpCzqecGnV/yAj4DQ=

# unseal 수행
VAULT_UNSEAL_KEY=$(jq -r ".unseal_keys_b64[]" cluster-keys.json)
kubectl exec vault-0 -n vault -- vault operator unseal $VAULT_UNSEAL_KEY
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false # Sealed 해제

# Pod 상태확인 -> Running이여야 함
kubectl get pod -n vault
NAME      READY   STATUS    RESTARTS   AGE
vault-0   1/1     Running   0          4h38m

vault pod의 경우 Readiness(exec [/bin/sh -ec vault status -tls-skip-verify])가 상태체크를 하는데 Sealed 가 해제가 안되면 실패로 되어 Pod 상태가 failed 가 된다.

현재 Sealed 가 해제되었으므로 정상적으로 Running상태를 유지한다.

valut cli 로 접속하기

# root 토큰 추출
jq -r ".root_token" cluster-keys.json
hvs.w5RaBb6Puc5KuANxSZbGqh2b
export VAULT_ADDR='http://localhost:30000'
vault status
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false

vault login
Token (will be hidden): # root 토큰 입력

secrets on k8s

k8s에서 vault에 있는 secret 정보를 가져오는 webapp 을 구현해보겠습니다. 전체적인 구조도는 아래와 같습니다.

우선 vault에 secret를 생성해줍니다.

vault secrets enable -path=secret kv-v2
vault kv put secret/webapp/config username="static-user" password="static-password"
# 확인
vault kv get secret/webapp/config

vault service account 확인

kubectl rbac-tool lookup vault
  SUBJECT | SUBJECT TYPE   | SCOPE       | NAMESPACE | ROLE                  | BINDING
----------+----------------+-------------+-----------+-----------------------+-----------------------
  vault   | ServiceAccount | ClusterRole |           | system:auth-delegator | vault-server-binding
  
kubectl rolesum vault -n vault
ServiceAccount: vault/vault
Secrets:

Policies:

• [CRB] */vault-server-binding ⟶  [CR] */system:auth-delegator
  Resource                                   Name  Exclude  Verbs  G L W C U P D DC
  subjectaccessreviews.authorization.k8s.io  [*]     [-]     [-]   ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖
  tokenreviews.authentication.k8s.io         [*]     [-]     [-]   ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖

vault sa에 kubernetes token을 검증할 수 있는 role 이 부과되어 있다.

kubernetes authentication method 활성화

vault auth enable kubernetes

# 확인
vault auth list
Path           Type          Accessor                    Description                Version
----           ----          --------                    -----------                -------
kubernetes/    kubernetes    auth_kubernetes_3f25d891    n/a                        n/a     # -> kubernetes 인증 추가
token/         token         auth_token_d62d3f8a         token based credentials    n/a

vault에 어떤 kubernetes를 인증할껀지 cluster를 지정해줘야한다. 현재 vault가 설치되어 있는 kubernetes 주소를 입력해준다. 만약 다른 cluster라면 cluster의 주소를 입력한다.

vault write auth/kubernetes/config kubernetes_host="https://kubernetes.default.svc

# 확인
vault read auth/kubernetes/config
Key                                  Value
---                                  -----
disable_iss_validation               true
disable_local_ca_jwt                 false
issuer                               n/a
kubernetes_ca_cert                   n/a
kubernetes_host                      https://kubernetes.default.svc
pem_keys                             []
token_reviewer_jwt_set               false

이후 클라이언트가 secret/webapp/config (아직 생성하지는 않음)에서 정의된 비밀 데이터에 접근하기 위해 path secret/data/webapp/config에 대한 읽기 기능이 부여된 policy를 생성합니다.

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

kubernetes 서비스 계정 이름과 웹앱 정책을 연결하는 웹앱이라는 이름의 kubernetes 인증 역할을 생성해줍니다.

정책 연결을 위해 sa 도 같이 생성해줍니다.

# sa 생성
kubectl create sa vault -n default

# 정책을 연결하는 역할
vault write auth/kubernetes/role/webapp \
        bound_service_account_names=vault \
        bound_service_account_namespaces=default \
        policies=webapp \
        ttl=24h \
        audience="https://kubernetes.default.svc.cluster.local"
Success! Data written to: auth/kubernetes/role/webapp

위 내용은 namespace default에 있는 service account vault와 policy는 webapp을 연결하고 토큰은 24시간동안 유효하다는 것을 의미합니다.

실제 webapp 애플리케이션을 배포하고 vault를 이용해서 인증처리를 하는 지 실습을 해보겠습니다.

cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  labels:
    app: webapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      serviceAccountName: vault
      containers:
        - name: app
          image: hashieducation/simple-vault-client:latest
          imagePullPolicy: Always
          env:
            - name: VAULT_ADDR
              value: 'http://vault.vault.svc:8200'
            - name: JWT_PATH
              value: '/var/run/secrets/kubernetes.io/serviceaccount/token'
            - name: SERVICE_PORT
              value: '8080'
          volumeMounts:
          - name: sa-token
            mountPath: /var/run/secrets/kubernetes.io/serviceaccount
            readOnly: true
      volumes:
      - name: sa-token
        projected:
          sources:
          - serviceAccountToken:
              path: token
              expirationSeconds: 600 # 10분 만료 , It defaults to 1 hour and must be at least 10 minutes (600 seconds)
---
apiVersion: v1
kind: Service
metadata:
  name: webapp
spec:
  selector:
    app: webapp
  type: NodePort
  ports:
  - port: 80
    targetPort: 8080
    protocol: TCP
    nodePort: 30001
EOF

# 배포확인
kubectl get pod -l app=webapp

위 배포 yaml을 살펴보면 아래와 같은 항목들이 있습니다.

      volumes:
      - name: sa-token
        projected:
          sources:
          - serviceAccountToken:
              path: token
              expirationSeconds: 600 # 10분 만료 , It defaults to 1 hour and must be at least 10 minutes (600 seconds)

위 설정은 서비스 계정 토큰의 시크릿 기반 볼륨 대신 projected volume 사용. 토큰을 사용하는 대상(audience), 유효 기간(expiration) 등 토큰의 속성을 지정할 필요가 있기 때문에 Service Account Token Volume Projection 를 사용합니다. 참조-1 참조-2

배포 이후 실제 토큰을 로드하는 지 확인해봅니다.

kubectl exec -it deploy/webapp -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
kubectl exec -it deploy/webapp -- cat /var/run/secrets/kubernetes.io/serviceaccount/token | cut -d '.' -f2 | base64 -d ; echo "\"}"
{"aud":["https://kubernetes.default.svc.cluster.local"],"exp":1764882573,"iat":1764881973,"iss":"https://kubernetes.default.svc.cluster.local","jti":"4acc3902-8060-4d78-a3a3-260542b95ab8","kubernetes.io":{"namespace":"default","node":{"name":"myk8s-control-plane","uid":"2e0af074-7bc8-4e65-9ad8-2add60113d0c"},"pod":{"name":"webapp-9484c6fd7-782bq","uid":"4c3818e5-f3e2-41b7-8e99-4dc87e3b28fb"},"serviceaccount":{"name":"vault","uid":"aaf76c55-ef56-43cb-91b3-57c7dc7ead3a"}},"nbf":1764881973,"sub":"system:serviceaccount:default:vault"}

실제 제대로 secret 정보를 가져오는 테스트를 해보고 로그도 확인해보면 아래와 같이 나옵니다.

curl 127.0.0.1:30001
password:static-password username:static-user

kubectl logs -l app=webapp -f
Retrieved token:  hvs.CAESIKrXQrz_W-uWSgPAkMrHBYmeHgfUtAerip4Soycigg9fGh4KHGh2cy5xZW1Xek50SUxtT0JXSVVlWkhxNEN3a1Y
2025/12/04 21:10:12 Received Request - Port forwarding is working.
Read JWT: eyJhbGciOiJSUzI1NiIsImtpZCI6IkR4dHh2UTg0VV95Q283c3RsMFdXd3JxVDYwYkcyZzkzLTNjZUZPTzRQQUEifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzY0ODgzMDg5LCJpYXQiOjE3NjQ4ODI0ODksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiYzc1MTE2Y2YtMTM2Zi00NGM4LWE0M2QtOTBkMjY5NjExYjQ0Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwibm9kZSI6eyJuYW1lIjoibXlrOHMtY29udHJvbC1wbGFuZSIsInVpZCI6IjJlMGFmMDc0LTdiYzgtNGU2NS05YWQ4LTJhZGQ2MDExM2QwYyJ9LCJwb2QiOnsibmFtZSI6IndlYmFwcC05NDg0YzZmZDctNzgyYnEiLCJ1aWQiOiI0YzM4MThlNS1mM2UyLTQxYjctOGU5OS00ZGM4N2UzYjI4ZmIifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6InZhdWx0IiwidWlkIjoiYWFmNzZjNTUtZWY1Ni00M2NiLTkxYjMtNTdjN2RjN2VhZDNhIn19LCJuYmYiOjE3NjQ4ODI0ODksInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OnZhdWx0In0.arj9M_bX9FYS8CghkiJFamey5kNRB3id8WKVUIn0D0N7u71L82Bu0Vytwslg-656I-UjH_aFx0K6xUICMfy5jQp6aYiYjUSs8ftuIbR_HYJ_RZEGTXOYnoD2hvUZoTzHVviY-6nVIMZ6sb-sK2JJZMEpBuUEZtILkoIvZS2S6p3CxHgMhpFsLp7yv-Bcl3ddj4ZKg6O4EkHVNhwzVxS0slMcJcwA8nMeCCpBUyJ0bvc5jRyU0a8LmuQ0lLQ2c4uR9kLOfFLVFZKcbMnx_Df5C7eHGf2842I9ienskPCCITzaiskNqKmZgnMAWS31HO2yxMITyYMORfwUmEswOjpTcg

로그에 나와 있는 jwt 를 확인해보면

{
  "aud": [
    "https://kubernetes.default.svc.cluster.local"
  ],
  "exp": 1764883089,
  "iat": 1764882489,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "jti": "c75116cf-136f-44c8-a43d-90d269611b44",
  "kubernetes.io": {
    "namespace": "default",
    "node": {
      "name": "myk8s-control-plane",
      "uid": "2e0af074-7bc8-4e65-9ad8-2add60113d0c"
    },
    "pod": {
      "name": "webapp-9484c6fd7-782bq",
      "uid": "4c3818e5-f3e2-41b7-8e99-4dc87e3b28fb"
    },
    "serviceaccount": {
      "name": "vault",
      "uid": "aaf76c55-ef56-43cb-91b3-57c7dc7ead3a"
    }
  },
  "nbf": 1764882489,
  "sub": "system:serviceaccount:default:vault"
}

위와 같이 나타나며 sa는 vault, namespace는 default 가 명시된 것을 확인 할 수 있습니다.

이번에는 secret 변경해서 제대로 로드 되는 지 테스트를 해보겠습니다.

vault kv put secret/webapp/config username="changed-user" password="changed-password"
====== Secret Path ======
secret/data/webapp/config

======= Metadata =======
Key                Value
---                -----
created_time       2025-12-04T21:11:35.302425054Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            2    # secret 변경으로 version 2로 변경됨

# 변경된 정보 확인
curl 127.0.0.1:30001
password:changed-password username:changed-user

vault secrets operator

VSO는 k8S Native Secret 를 업데이트 및 관리, 개발자가 Vault 도구 학습할 필요가 없습니다.

  • 기존에 Vault 사용을 위해 Vault Login, Vault Secret Read 등에 대한 동작을 애플리케이션에서 구현할 필요 없이, VSO가 대신 수행.
  • VSO는 Vault 의 Secret 를 k8S Native Secret 에 동기화.
    • Deployment, ReplicaSet, StatefulSet, Argo Rollout Kubernetes 리소스 유형에 대한 Rollout 으로 자동 시크릿 교체 적용 가능
      • 물론 Rollout 하지 않고, 애플리케이션에서 변경된 값을 반영하게 구성 가능함.

Quick install

테스트를 위한 클러스터는 아래와 같이 구성하고 vault는 dev모드를 활성화 해줍니다.

# 클러스터 구성
kind create cluster --name myk8s --image kindest/node:v1.32.8 --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  labels:
    ingress-ready: true
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP
  - containerPort: 30000  # Vault Web UI
    hostPort: 30000
  - containerPort: 30001  # Sample application
    hostPort: 30001
EOF
docker exec -it myk8s-control-plane sh -c 'apt update && apt install tree psmisc lsof wget net-tools dnsutils tcpdump ngrep iputils-ping git vim -y'


# vault 설치 dev 모드 활성화
helm install vault hashicorp/vault -n vault --create-namespace \
  --set server.image.repository=hashicorp/vault \
  --set server.image.tag=1.19.0 \
  --set server.dev.enabled=true \
  --set server.dev.devRootToken=root \
  --set server.logLevel=debug \
  --set server.service.enabled=true \
  --set server.service.type=ClusterIP \
  --set server.service.port=8200 \
  --set server.service.targetPort=8200 \
  --set ui.enabled=true \
  --set ui.serviceType=NodePort \
  --set ui.externalPort=8200 \
  --set ui.serviceNodePort=30000 \
  --set injector.enabled=false \
  --version 0.30.0
  
# 확인
kubectl get pods -n vault

vault 설정을 위해서 vault cli 로 초기 설정을 진행합니다. 아래의 작업들을 순서대로 진행해줍니다.

  • kubernetes 인증 활성화
  • 시크릿(엔진v2) 생성
  • policy 생성
  • 역할 맵핑
  • secrets 생성
# vault login
export VAULT_ADDR='http://localhost:30000'
vault login
Token (will be hidden): root

# kubernetes 인증 활성화
vault auth enable -path demo-auth-mount kubernetes

# kubernetes cluster 설정
vault write auth/demo-auth-mount/config kubernetes_host="https://kubernetes.default.svc"

# 시크릿(엔진v2) 생성
vault secrets enable -path=kvv2 kv-v2
vault kv put kvv2/webapp/config username="static-user" password="static-password"

# 정책생성
vault policy write webapp - <<EOF
path "kvv2/data/webapp/config" {
  capabilities = ["read"]
}
EOF

# 역할 맵핑
vault write auth/demo-auth-mount/role/role1 \
   bound_service_account_names=demo-static-app \
   bound_service_account_namespaces=app \
   policies=webapp \
   audience=vault \
   ttl=24h

vault secrets operator 설치를 진행해봅니다. (helm v4 버전으로 설치 시 validation 강화로 에러가 발생한다고 합니다. 저는 helm v3를 사용하고 있어서 그냥 진행하도록 하겠습니다)

# Helm v3 설치
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
helm version
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update

helm search repo hashicorp/vault
NAME                            	CHART VERSION	APP VERSION	DESCRIPTION
hashicorp/vault                 	0.31.0       	1.20.4     	Official HashiCorp Vault Chart
hashicorp/vault-secrets-gateway 	0.0.2        	0.1.0      	A Helm chart for Kubernetes
hashicorp/vault-secrets-operator	1.0.1        	1.0.1      	Official Vault Secrets Operator Chart

helm install vault-secrets-operator hashicorp/vault-secrets-operator -n vault-secrets-operator-system --create-namespace \
  --set defaultVaultConnection.enabled=true \
  --set defaultVaultConnection.address=http://vault.vault.svc.cluster.local:8200 \
  --set defaultVaultConnection.skipTLSVerify=false \
  --set controller.manager.clientCache.persistenceModel=direct-encrypted \
  --set controller.manager.clientCache.storageEncryption.enabled=true \
  --set controller.manager.clientCache.storageEncryption.mount=k8s-auth-mount \
  --set controller.manager.clientCache.storageEncryption.keyName=vso-client-cache \
  --set controller.manager.clientCache.storageEncryption.transitMount=demo-transit \
  --set controller.manager.clientCache.storageEncryption.kubernetes.role=auth-role-operator \
  --set controller.manager.clientCache.storageEncryption.kubernetes.serviceAccount=vault-secrets-operator-controller-manager \
  --set 'controller.manager.clientCache.storageEncryption.kubernetes.tokenAudiences[0]=vault' \
  --version 0.10.0
NAME: vault-secrets-operator
LAST DEPLOYED: Fri Dec  5 06:55:30 2025
NAMESPACE: vault-secrets-operator-system
STATUS: deployed
REVISION: 1

# 설치확인
kubectl get-all -n vault-secrets-operator-system

vso 를 설치하면 아래의 crd 들이 생깁니다. 해당 crd를 활용하여 vault 인증에 사용됩니다. 대표적으로는 vaultconnections, vaultauths crd를 사용합니다.

kubectl get crd | grep secrets.hashicorp.com
hcpauths.secrets.hashicorp.com                2025-12-04T21:55:30Z
hcpvaultsecretsapps.secrets.hashicorp.com     2025-12-04T21:55:30Z
secrettransformations.secrets.hashicorp.com   2025-12-04T21:55:30Z
vaultauthglobals.secrets.hashicorp.com        2025-12-04T21:55:30Z
vaultauths.secrets.hashicorp.com              2025-12-04T21:55:30Z
vaultconnections.secrets.hashicorp.com        2025-12-04T21:55:30Z
vaultdynamicsecrets.secrets.hashicorp.com     2025-12-04T21:55:30Z
vaultpkisecrets.secrets.hashicorp.com         2025-12-04T21:55:30Z
vaultstaticsecrets.secrets.hashicorp.com      2025-12-04T21:55:30Z

kubectl get vaultconnections,vaultauths -n vault-secrets-operator-system
NAME                                            AGE
vaultconnection.secrets.hashicorp.com/default   2m5s
NAME                                                                          AGE
vaultauth.secrets.hashicorp.com/vault-secrets-operator-default-transit-auth   2m5s

# vaultconnection CRD 확인
kubectl get vaultconnections -n vault-secrets-operator-system default -o jsonpath='{.spec}' | jq
{
  "address": "http://vault.vault.svc.cluster.local:8200",
  "skipTLSVerify": false
}

# vaultauth CRD 확인
kubectl get vaultauth -n vault-secrets-operator-system vault-secrets-operator-default-transit-auth -o jsonpath='{.spec}' | jq
{
  "kubernetes": {
    "audiences": [
      "vault"
    ],
    "role": "auth-role-operator",
    "serviceAccount": "vault-secrets-operator-controller-manager",
    "tokenExpirationSeconds": 600
  },
  "method": "kubernetes",
  "mount": "k8s-auth-mount",
  "storageEncryption": {
    "keyName": "vso-client-cache",
    "mount": "demo-transit"
  },
  "vaultConnectionRef": "default"
}

# VSO 파드에 서비스 어카운트가 사용 가능한 Role 확인
kubectl rbac-tool lookup vault-secrets-operator-controller-manager

  SUBJECT                                   | SUBJECT TYPE   | SCOPE       | NAMESPACE                     | ROLE                                        | BINDING
--------------------------------------------+----------------+-------------+-------------------------------+---------------------------------------------+-----------------------------------------------------
  vault-secrets-operator-controller-manager | ServiceAccount | ClusterRole |                               | vault-secrets-operator-manager-role         | vault-secrets-operator-manager-rolebinding
  vault-secrets-operator-controller-manager | ServiceAccount | ClusterRole |                               | vault-secrets-operator-proxy-role           | vault-secrets-operator-proxy-rolebinding
  vault-secrets-operator-controller-manager | ServiceAccount | Role        | vault-secrets-operator-system | vault-secrets-operator-leader-election-role | vault-secrets-operator-leader-election-rolebinding

# vault-secrets-operator-controller-manager 를 확인해보면 secrets 에 대해서 full 권한을 가진 것을 확인해볼 수 있다.
# VSO는 deployment 등에 Secret 적용을 위한 rollout(G W P U) 필요, 특히 vault 서버로 부터 암호 값을 가져와서 secret 에 업데이트 및 관리 필요함.
kubectl rolesum -n vault-secrets-operator-system vault-secrets-operator-controller-manager
  secrets                                                 [*]     [-]     [-]   ✔ ✔ ✔ ✔ ✔ ✔ ✔ ✔

vso 설치를 끝냈으니 webapp 에서 사용될 service account를 생성 해줍니다.

kubectl create ns app
cat << EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  # SA bound to the VSO namespace for transit engine auth
  namespace: vault-secrets-operator-system
  name: demo-operator
---
apiVersion: v1
kind: ServiceAccount
metadata:
  namespace: app
  name: demo-static-app
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
  name: static-auth
  namespace: app
spec:
  method: kubernetes
  mount: demo-auth-mount
  kubernetes:
    role: role1
    serviceAccount: demo-static-app
    audiences:
      - vault
EOF

VaultStaticSecret crd를 생성해줍니다.

  • VaultStaticSecret은 VSO의 Custom Resource로, Vault의 정적 시크릿을 Kubernetes Secret으로 동기화합니다.
  • Vault에 저장된 정적 시크릿을 Kubernetes Secret으로 자동 동기화
cat << EOF | kubectl apply -f -
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
  name: vault-kv-app
  namespace: app
spec:
  # secret version
  type: kv-v2

  # mount path
  mount: kvv2

  # path of the secret
  path: webapp/config

  # dest k8s secret
  destination:
    name: secretkv
    create: true

  # static secret refresh interval 시크릿 리프레시 주기
  refreshAfter: 30s

  # Name of the CRD to authenticate to Vault
  vaultAuthRef: static-auth
EOF

# 확인
kubectl get vaultstaticsecret -n app
NAME           AGE
vault-kv-app   21s
  • refreshAfter 30s 로 설정되었기에 vault에서 변경 시 30s 마다 kubernetes secret에 반영됨

static secrets rotate 테스트를 진행해보겠습니다.

# destination을 secretkv로 했기에 app ns에서 secretkv가 생성되었습니다.
kubectl get secret -n app
NAME       TYPE     DATA   AGE
secretkv   Opaque   3      16m

# secretkv 를 확인해보면 webapp/config에 있는 vault secret 정보를 가지고 있습니다.
kubectl krew install view-secret
kubectl view-secret -n app secretkv --all
_raw='{"data":{"password":"static-password","username":"static-user"},"metadata":{"created_time":"2025-12-04T21:45:21.758055505Z","custom_metadata":null,"deletion_time":"","destroyed":false,"version":1}}'
password='static-password'
username='static-user'

secretkv에 webapp/config 의 secrets가 저장되어 있습니다. vault secret을 업데이트 하고 실제로 sa 에 변경되는 지 확인해보겠습니다.

vault kv put kvv2/webapp/config username="static-user2" password="static-password2"
===== Secret Path =====
kvv2/data/webapp/config

======= Metadata =======
Key                Value
---                -----
created_time       2025-12-05T03:51:38.360539428Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            2    # version2

# secret 업데이트 이후 조회하면 변경된 secret 정보가 반영된 것을 확인할 수 잇습니다.
kubectl view-secret -n app secretkv --all
_raw='{"data":{"password":"static-password2","username":"static-user2"},"metadata":{"created_time":"2025-12-05T03:51:38.360539428Z","custom_metadata":null,"deletion_time":"","destroyed":false,"version":2}}'
password='static-password2'
username='static-user2'

# secret을 보면 새로 생성하지는 않고 값만 업데이트함을 age를 통해 확인할 수 있음.
kubectl get secret -n app
NAME       TYPE     DATA   AGE
secretkv   Opaque   3      19m

 

PostgreSQL 파드 배포 및 Vault Database Secret Engine 설정

vault를 database와 연동하여 진행하는 실습을 해보겠습니다.

kubectl create ns postgres

# postgres 설치
helm repo add bitnami https://charts.bitnami.com/bitnami
helm upgrade --install postgres bitnami/postgresql --namespace postgres --set auth.audit.logConnections=true  --set auth.postgresPassword=secret-pass

# 설치확인
kubectl get sts,pod,svc,ep,pvc,secret -n postgres
kubectl view-secret -n postgres postgres-postgresql --all

# db 확인
kubectl exec -it -n postgres postgres-postgresql-0 -- sh -c "PGPASSWORD=secret-pass psql -U postgres -h localhost -c '\l'"

postgres에서 vault secret 연동을 위한 설정을 해보겠습니다.

# 먼저 database secret engine을 활성화 시켜줍니다.
vault secrets enable -path=demo-db database

# vault 에 DB에 대한 정보 설정 (DB 사용자 이름, 암호)
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 등록
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="10m" \
   max_ttl="20m

# 정책생성
vault policy write demo-auth-policy-db - <<EOF
path "demo-db/creds/dev-postgres" {
   capabilities = ["read"]
}
EOF

동적 시크릿을 셋업해보겠습니다.

vault write auth/demo-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

동적 시크릿 생성까지 마쳤으니 demo-ns 네임스페이스에 vso-db-demo 파드가 동적 암호를 사용할 수 있게 해보겠습니다.

# 예제 배포를 위해 소스코드를 다운 받아 줍니다.
git clone https://github.com/hashicorp-education/learn-vault-secrets-operator
cd learn-vault-secrets-operator

# dynamic-secrets 관련 리소스 배포
kubectl create ns demo-ns
kubectl apply -f dynamic-secrets/.

# 확인
kubectl get pod -n demo-ns

# pod 내에 secret 정보확인
kubectl exec -it deploy/vso-db-demo -n demo-ns -- ls -al /etc/secrets
kubectl exec -it deploy/vso-db-demo -n demo-ns -- cat /etc/secrets/username ; echo
kubectl exec -it deploy/vso-db-demo -n demo-ns -- cat /etc/secrets/password ; echo

# k8s secret 확인
kubectl view-secret -n demo-ns vso-db-demo --all
_raw='{"password":"-7PrAIpXTPuhgAud1zOk","username":"v-demo-aut-dev-post-F4IubNIZJgA3yxOSn3eS-1764909190"}'
password='-7PrAIpXTPuhgAud1zOk'
username='v-demo-aut-dev-post-F4IubNIZJgA3yxOSn3eS-1764909190'

Vault 가 Psql 암호를 동적으로 변경하고 VSO가 해당 암호를 K8S Secret 동기화 관련 상세 확인해보겠습니다.

postgres에 주기적으로 role name이 쌓이고, secret은 업데이트 되며, pod에는 업데이트 secret 반영 시 rollout을 통해 업데이트 된 값을 반영하는 방식으로 동기화가 진행됩니다.

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-demo-aut-dev-post-6S7DF0WhoAZdInsPvZUN-1764908346 | Password valid until 2025-12-05 04:39:11+00
 v-demo-aut-dev-post-F4IubNIZJgA3yxOSn3eS-1764909190 | Password valid until 2025-12-05 04:50:01+00
 v-demo-aut-dev-post-mvGivMJamb14Pnb8k6ix-1764909167 | Password valid until 2025-12-05 04:49:50+00
 v-demo-aut-dev-post-nQbIqxaHsIXyjA4KstmU-1764908346 | Password valid until 2025-12-05 04:29:11+00
 v-demo-aut-dev-post-o3EqFmRNzHMllt3U2nOg-1764908347 | Password valid until 2025-12-05 04:39:12+00
 v-demo-aut-dev-post-sHAKjT3I7A6DmEaDyrtO-1764908346 | Password valid until 2025-12-05 04:29:11+00

# 현재 secret age 상태 25m
kubectl get secret -n demo-ns
NAME                  TYPE     DATA   AGE
vso-db-demo           Opaque   3      25m
vso-db-demo-created   Opaque   3      25m

# 현재 시점에서 동기화된 postgres username: v-demo-aut-dev-post-F4IubNIZJgA3yxOSn3eS-1764909190
kubectl view-secret -n demo-ns vso-db-demo --all
_raw='{"password":"-7PrAIpXTPuhgAud1zOk","username":"v-demo-aut-dev-post-F4IubNIZJgA3yxOSn3eS-1764909190"}'
password='-7PrAIpXTPuhgAud1zOk'
username='v-demo-aut-dev-post-F4IubNIZJgA3yxOSn3eS-1764909190'

# (5분 정도 이후) 2차 K8S Secret 확인
# secret 리소스가 재생성되지는 않았고, Data 값만 바뀌었다
kubectl get secret -n demo-ns
NAME                  TYPE     DATA   AGE
vso-db-demo           Opaque   3      30m
vso-db-demo-created   Opaque   3      30m

# 변경된 postgres username: v-demo-aut-dev-post-clfCeAzddy7Hyhr53enX-1764910006
# 주기적으로 동기화하고 있음을 알 수 있다.
kubectl view-secret -n demo-ns vso-db-demo --all
_raw='{"password":"EQJKEfS7D5-hLsQtxQGf","username":"v-demo-aut-dev-post-clfCeAzddy7Hyhr53enX-1764910006"}'
password='EQJKEfS7D5-hLsQtxQGf'
username='v-demo-aut-dev-post-clfCeAzddy7Hyhr53enX-1764910006'

# AGE를 보면 파드가 rollout 되었음을 알 수 있다
kubectl get pod -n demo-ns
NAME                          READY   STATUS    RESTARTS   AGE
vso-db-demo-58c59b765-hlbhl   1/1     Running   0          2m22s
vso-db-demo-58c59b765-jbsbc   1/1     Running   0          2m25s
vso-db-demo-58c59b765-pf8r5   1/1     Running   0          2m25s
kubectl describe deploy -n demo-ns

# 실제 postgresql 에 사용자 정보 확인 : 계속 추가되고 있음..
kubectl exec -it -n postgres postgres-postgresql-0 -- sh -c "PGPASSWORD=secret-pass psql -U postgres -h localhost -c '\du'"

Vault PKI + cert-manager를 통한 동적 인증서 관리

Vault PKI Secrets Engine과 cert-manager를 연동해 Kubernetes에서 TLS 인증서를 자동 발급·갱신합니다. 인증서는 Vault에서 생성되고 Kubernetes Secret으로 저장되어 애플리케이션에서 사용됩니다.

Vault PKI Secrets Engine 설정

PKI 엔진 활성화

vault secrets enable pki
  • PKI Secrets Engine을 pki/ 경로에 활성화
  • 인증서 발급, 서명, 갱신, 폐기 관리

TTL 설정

vault secrets tune -max-lease-ttl=8760h pki
  • 기본 TTL(30일)을 8760시간(1년)으로 변경
  • 발급 가능한 최대 유효기간 설정

Root CA 생성

vault write pki/root/generate/internal \
    common_name=example.com \
    ttl=8760h
  • 자체 서명 Root CA 생성
  • example.com을 Common Name으로 사용
  • 8760시간 유효
  • 참고: 프로덕션에서는 외부 Root CA를 사용하고 Vault에는 Intermediate CA를 제공하는 것을 권장

CA 및 CRL 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"

# CA 인증서 확인
open "http://127.0.0.1:30000/v1/pki/ca"
openssl x509 -noout -text -in ~/Downloads/ca

# CRL 확인
open "http://127.0.0.1:30000/v1/pki/crl"
openssl crl -noout -text -in ~/Downloads/crl
  • 발급 인증서에 AIA/CDP 확장 필드 포함
  • 클라이언트가 CA 인증서와 CRL을 자동으로 가져와 검증

PKI Role 생성

vault write pki/roles/example-dot-com \
    allowed_domains=example.com \
    allow_subdomains=true \
    max_ttl=72h
  • Role: 인증서 발급 정책의 논리적 이름
  • allowed_domains: 허용 도메인
  • allow_subdomains: 서브도메인 허용 (예: www.example.com, api.example.com)
  • max_ttl: 발급 인증서 최대 유효기간 (72시간)

Policy 생성

vault policy write pki - <<EOF
path "pki*"                        { capabilities = ["read", "list"] }
path "pki/sign/example-dot-com"    { capabilities = ["create", "update"] }
path "pki/issue/example-dot-com"   { capabilities = ["create"] }
EOF
  • pki*: PKI 엔진 경로 읽기/목록 조회
  • pki/sign/example-dot-com: 인증서 서명
  • pki/issue/example-dot-com: 인증서 발급

Kubernetes 인증 설정

Kubernetes Auth 활성화

vault auth enable kubernetes
  • Kubernetes ServiceAccount 기반 인증 활성화

Kubernetes Auth 설정

vault write auth/kubernetes/config kubernetes_host="https://kubernetes.default.svc"
  • Kubernetes API 서버 주소 설정
  • Vault가 ServiceAccount 토큰 검증

Kubernetes Role 생성

vault write auth/kubernetes/role/issuer \
    bound_service_account_names=issuer \
    bound_service_account_namespaces=default \
    policies=pki \
    ttl=20m
  • bound_service_account_names: 허용 ServiceAccount 이름
  • bound_service_account_namespaces: 허용 네임스페이스
  • policies: 부여할 Vault 정책 (pki)
  • ttl: 인증 토큰 유효기간 (20분)

cert-manager 설치 및 설정

cert-manager CRD 설치

kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.12.3/cert-manager.crds.yaml
  • cert-manager Custom Resource Definitions 설치

cert-manager 설치

helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager --namespace cert-manager --create-namespace --version v1.12.3 jetstack/cert-manager
  • Helm으로 cert-manager 설치
  • 인증서 발급/갱신 자동화

ServiceAccount 및 Token 생성

ServiceAccount 생성

kubectl create serviceaccount issuer
  • issuer ServiceAccount 생성
  • Vault 인증에 사용

Token Secret 생성

cat << EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
  name: issuer-token-lmzpj
  annotations:
    kubernetes.io/service-account.name: issuer
type: kubernetes.io/service-account-token
EOF
  • ServiceAccount 토큰을 담은 Secret 생성
  • cert-manager가 이 토큰으로 Vault 인증

Token Secret 이름 확인

ISSUER_SECRET_REF=$(kubectl get secrets --output=json | jq -r '.items[].metadata | select(.name|startswith("issuer-token-")).name')
echo $ISSUER_SECRET_REF
  • 동적으로 생성된 Secret 이름을 변수에 저장

Issuer 리소스 생성

Vault Issuer 생성

cat << EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: vault-issuer
  namespace: default
spec:
  vault:
    server: http://vault.vault.svc:8200
    path: pki/sign/example-dot-com
    auth:
      kubernetes:
        mountPath: /v1/auth/kubernetes
        role: issuer
        secretRef:
          name: $ISSUER_SECRET_REF
          key: token
EOF
  • server: Vault 서버 주소
  • path: 인증서 서명 경로 (pki/sign/example-dot-com)
  • auth.kubernetes: Kubernetes 인증 설정
    • mountPath: Kubernetes Auth 마운트 경로
    • role: Vault Kubernetes Role 이름
    • secretRef: ServiceAccount 토큰이 담긴 Secret 참조

Certificate 리소스 생성

Certificate 생성

cat << EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com
  namespace: default
spec:
  secretName: $ISSUER_SECRET_REF
  issuerRef:
    name: vault-issuer
  commonName: www.example.com
  dnsNames:
  - www.example.com
EOF
  • secretName: 인증서가 저장될 Secret 이름
  • issuerRef: 사용할 Issuer 참조
  • commonName: 인증서 CN
  • dnsNames: SAN DNS 이름 목록

인증서 상태 확인

kubectl get certificate.cert-manager.io/example-com -owide
kubectl describe certificate.cert-manager example-com

Secret 확인

kubectl view-secret $ISSUER_SECRET_REF --all

Secret 내용

  • tls.crt: 인증서
  • tls.key: 개인키
  • ca.crt: CA 인증서

전체 프로세스 흐름

  1. Vault 설정
    • PKI 엔진 활성화 → Root CA 생성 → Role/Policy 생성
  2. Kubernetes 인증 설정
    • Kubernetes Auth 활성화 → Role 생성
  3. cert-manager 설정
    • cert-manager 설치 → ServiceAccount/Token 생성 → Issuer 생성
  4. 인증서 발급
    • Certificate 리소스 생성 → cert-manager가 Vault 인증 → 인증서 발급 요청 → Secret 생성/업데이트
  5. 자동 갱신
    • cert-manager가 만료 전 자동 갱신 → Secret 업데이트 → 애플리케이션에 반영

주요 장점

  1. 자동화: 인증서 발급/갱신 자동화
  2. 보안: Vault에서 중앙 관리
  3. 동적 갱신: 만료 전 자동 갱신
  4. Kubernetes 통합: 네이티브 리소스로 관리
  5. 정책 기반: Role/Policy로 제어

 

Vault Production

이번 테스트에서는 vault production 모드를 어떻게 구축하고 사용하는지에 대해서 알아보겠습니다.

클러스터 구성

kind create cluster --name myk8s --image kindest/node:v1.32.8 --config - <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  labels:
    ingress-ready: true
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP
  - containerPort: 30000
    hostPort: 30000
  - containerPort: 30001
    hostPort: 30001
- role: worker
- role: worker
- role: worker
EOF

ingres 배포

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml

# nodeSelector 지정
kubectl patch deployment ingress-nginx-controller -n ingress-nginx \
  --type='merge' \
  -p='{
    "spec": {
      "template": {
        "spec": {
          "nodeSelector": {
            "ingress-ready": "true"
          }
        }
      }
    }
  }'

# SSL Passthrough flag 활성화 설정 https://kubernetes.github.io/ingress-nginx/user-guide/tls/#ssl-passthrough
kubectl patch deployment ingress-nginx-controller -n ingress-nginx --type='json' -p='[
  {
    "op": "add",
    "path": "/spec/template/spec/containers/0/args/-",
    "value": "--enable-ssl-passthrough"
  }
]'

vault 설치

helm install vault hashicorp/vault -n vault \
  --create-namespace \
  --set server.replicas=3 \
  --set server.ha.enabled=true \
  --set server.ha.replicas=3 \
  --set server.ha.raft.enabled=true \
  --set 'server.ha.config=ui = true
listener "tcp" {
  tls_disable = 1
  address = "[::]:8200"
  cluster_address = "[::]:8201"
}
service_registration "kubernetes" {}' \
  --set server.readinessProbe.enabled=true \
  --set server.dataStorage.enabled=true \
  --set server.dataStorage.size=10Gi \
  --set server.service.enabled=true \
  --set server.service.type=ClusterIP \
  --set server.service.port=8200 \
  --set server.service.targetPort=8200 \
  --set ui.enabled=true \
  --set ui.serviceType=NodePort \
  --set ui.externalPort=8200 \
  --set ui.serviceNodePort=30000 \
  --set injector.enabled=false \
  --version 0.31.0
 
# 로그확인
kubectl stern -n vault vault-0

vault 초기화 작업

vault production mode 설치가 끝나면 sealed 되어 있기 때문에 unseal을 해줘야 한다. root 토큰은 나중에 사용하니 저장해준다.

# vault 초기화
kubectl exec -it vault-0 -n vault -- sh
vault operator init
Unseal Key 1: epmrRLlqiS6jD+A7CGEIF7GIWTbhS/9tqnArNH42pRy0
Unseal Key 2: CK6n6/SWsoT3HmXZT92ak/eByHSHTwcm5V3S8bX9p1XD
Unseal Key 3: BVaiCT9O5iLUoW2/IL2eaz6FuY3XpWqldLpe/CTVNnj+
Unseal Key 4: MtTxs5puj5MlHP7zcXe/onvGAz6CE+YgdGg8rd7eZxv/
Unseal Key 5: RORUpkoy2tCTuHHMACWsJg9subG5NYyxB1Dcse3HmC/I
Initial Root Token: hvs.trmJPVHrjmTKOKH3gSBeFudX # -> root 토큰은 저장해두자

vault status
Key                     Value
---                     -----
Seal Type               shamir
Initialized             true
Sealed                  true
Total Shares            5

위 명령어로 조회 시 unseal 5개 key중 3개의 키를를 반복적으로 입력해서 unseal을 수행해줘야 한다. 아래와 같이 반복적으로 3개 키 모두 진행한다.

vault operator unseal
Unseal Key (will be hidden): m3ZdD5M66/5g859TEaGNxKfxecbD03Gq0Mp3eb894XaP # 첫번째 키 입력
Key                     Value
---                     -----
Seal Type               shamir
Initialized             true
Sealed                  true
Total Shares            5
Threshold               3
Unseal Progress         1/3 # -> 진행할 수록 2/3, 3/3 변함
...

vault status
Key                     Value
---                     -----
Seal Type               shamir
Initialized             true
Sealed                  false # -> Sealed 해제
HA Enabled              true
HA Cluster              https://vault-0.vault-internal:8201 # -> 완료 된 경우
HA Mode                 active

vault-1, vault-2 를 vault-0에 join시킨다. join을 해야 vault-0에서 생성된 unseal key로 다른 vault에서도 unseal을 진행 할 수 있다.

kubectl exec -n vault -it vault-1 -- vault operator raft join http://vault-0.vault-internal:8200
kubectl exec -n vault -it vault-2 -- vault operator raft join http://vault-0.vault-internal:8200

vault ha 모드에서 총 3개의 pod 가 생성되는 데 unseal 작업을 vault-0 에서 진행했으므로 나머지 vault-1, vault-2 에도 똑같이 진행해준다. 아래에서 보듯이 vault-0 은 Ready Pod가 올라왔고 나머지는 아직 Ready 파드가 안올라왔다.

로그에서도 보듯이 vault-1 은 seal configuration missing 로그가 보인다.

kubectl get pods --selector='app.kubernetes.io/name=vault' -n vault
NAME      READY   STATUS    RESTARTS   AGE
vault-0   1/1     Running   0          8m13s
vault-1   0/1     Running   0          8m13s
vault-2   0/1     Running   0          8m13s

k stern -n vault vault-1
vault-1 vault 2025-12-05T08:06:51.439Z [INFO]  core: security barrier not initialized
vault-1 vault 2025-12-05T08:06:51.439Z [INFO]  core: seal configuration missing, not initialized

# vault-1
# vault-0에서의 unseal key를 사용한다.
kubectl exec -it vault-1 -n vault -- sh
vault operator unseal
vault operator unseal
vault operator unseal
vault status
HA Cluster              https://vault-0.vault-internal:8201

# vault-2
# vault-0에서의 unseal key를 사용한다.
kubectl exec -it vault-2 -n vault -- sh
vault operator unseal
vault operator unseal
vault operator unseal
vault status
HA Cluster              https://vault-0.vault-internal:8201

다 수행하고 나서 vault 의 running 파드가 다 올라왔는지 인지 확인한다.

kubectl get pods --selector='app.kubernetes.io/name=vault' -n vault
NAME      READY   STATUS    RESTARTS   AGE
vault-0   1/1     Running   0          16m
vault-1   1/1     Running   0          16m
vault-2   1/1     Running   0          16m

vault-0 에서 생성된 Root 토큰을 가지고 호스트 쉘에서 접근으로 사용한다.

export VAULT_ROOT_TOKEN=hvs.trmJPVHrjmTKOKH3gSBeFudX
export VAULT_ADDR='http://localhost:30000'
vault login
Token (will be hidden): hvs.trmJPVHrjmTKOKH3gSBeFudX # vault-2에서 생성된 root 토큰

vault operator raft list-peers
Node                                    Address                        State       Voter
----                                    -------                        -----       -----
b97e06b4-3556-f666-b524-82696fcf32a5    vault-0.vault-internal:8201    leader      true
ebcf7b53-00b4-594b-eb21-28128c92f4be    vault-1.vault-internal:8201    follower    true
2a2b1a95-72e5-c51e-f60e-33bb04214951    vault-2.vault-internal:8201    follower    true

다른 vault 들이 join한 것을 확인 할 수 있고 vault-0 가 leader이다.

이번에는 ldap 과 vault를 연동해보는 실습을 진행해보겠습니다.

ldap 서버 배포를 진행합니다.

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Namespace
metadata:
  name: openldap
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: openldap
  namespace: openldap
spec:
  replicas: 1
  selector:
    matchLabels:
      app: openldap
  template:
    metadata:
      labels:
        app: openldap
    spec:
      containers:
        - name: openldap
          image: osixia/openldap:1.5.0
          ports:
            - containerPort: 389
              name: ldap
            - containerPort: 636
              name: ldaps
          env:
            - name: LDAP_ORGANISATION    # 기관명, LDAP 기본 정보 생성 시 사용
              value: "Example Org"
            - name: LDAP_DOMAIN          # LDAP 기본 Base DN 을 자동 생성
              value: "example.org"
            - name: LDAP_ADMIN_PASSWORD  # LDAP 관리자 패스워드
              value: "admin"
            - name: LDAP_CONFIG_PASSWORD
              value: "admin"
        - name: phpldapadmin
          image: osixia/phpldapadmin:0.9.0
          ports:
            - containerPort: 80
              name: phpldapadmin
          env:
            - name: PHPLDAPADMIN_HTTPS
              value: "false"
            - name: PHPLDAPADMIN_LDAP_HOSTS
              value: "openldap"   # LDAP hostname inside cluster
---
apiVersion: v1
kind: Service
metadata:
  name: openldap
  namespace: openldap
spec:
  selector:
    app: openldap
  ports:
    - name: phpldapadmin
      port: 80
      targetPort: 80
      nodePort: 30001
    - name: ldap
      port: 389
      targetPort: 389
    - name: ldaps
      port: 636
      targetPort: 636
  type: NodePort
EOF

# 확인
kubectl get deploy,pod,svc,ep -n openldap

# 기본 LDAP 정보 : 아래 Bind DN과 PW로 로그인
## Base DN: dc=example,dc=org
## Bind DN: cn=admin,dc=example,dc=org
## Password: admin
open http://127.0.0.1:30001

# 로그 확인
kubectl logs -n openldap -l app=openldap -c phpldapadmin -f # phpLDAPadmin
kubectl logs -n openldap -l app=openldap -c openldap -f     # openldap

ldap 조직도 구성

kubectl -n openldap exec -it deploy/openldap -c openldap -- bash

# ldapadd로 ou 추가 (organizationalUnit)
cat <<EOF | ldapadd -x -D "cn=admin,dc=example,dc=org" -w admin
dn: ou=people,dc=example,dc=org
objectClass: organizationalUnit
ou: people

dn: ou=groups,dc=example,dc=org
objectClass: organizationalUnit
ou: groups
EOF

# ldapadd로 users 추가 (inetOrgPerson) : alice , bob
cat <<EOF | ldapadd -x -D "cn=admin,dc=example,dc=org" -w admin
dn: uid=alice,ou=people,dc=example,dc=org
objectClass: inetOrgPerson
cn: Alice
sn: Kim
uid: alice
mail: alice@example.org
userPassword: alice123

dn: uid=bob,ou=people,dc=example,dc=org
objectClass: inetOrgPerson
cn: Bob
sn: Lee
uid: bob
mail: bob@example.org
userPassword: bob123
EOF

# ldapadd로 groups 추가 (groupOfNames) : devs, admins
cat <<EOF | ldapadd -x -D "cn=admin,dc=example,dc=org" -w admin
dn: cn=devs,ou=groups,dc=example,dc=org
objectClass: groupOfNames
cn: devs
member: uid=bob,ou=people,dc=example,dc=org

dn: cn=admins,ou=groups,dc=example,dc=org
objectClass: groupOfNames
cn: admins
member: uid=alice,ou=people,dc=example,dc=org
EOF

# 빠져나오기
exit

Vault Auth with LDAP 설정하기

# ldap 엔진 활성화
vault auth enable ldap

# ldap config
vault write auth/ldap/config \
    url="ldap://openldap.openldap.svc:389" \
    starttls=false \
    insecure_tls=true \
    binddn="cn=admin,dc=example,dc=org" \
    bindpass="admin" \
    userdn="ou=people,dc=example,dc=org" \
    groupdn="ou=groups,dc=example,dc=org" \
    groupfilter="(member=uid={{.Username}},ou=people,dc=example,dc=org)" \
    groupattr="cn"

ldap 하고 잘 연동이 되었는 지 테스트하기 위해서 alice 계정으로 vault에 로그인을 시도해봅니다.

# LDAP 인증 테스트 :  
vault login -method=ldap username=alice
Password (will be hidden): alice123
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.CAESIIvb-vnlWyS5iuoYNQdkV27X3OBycxVWbDEf5jsbSsRlGh4KHGh2cy4wNG16RVZOSDI2RnNreDNPdmNtTGpscGw
token_accessor         n89HBfpnHyHvYR8Ua9lzZVZs
token_duration         768h
token_renewable        true
token_policies         ["default"]
identity_policies      []
policies               ["default"]
token_meta_username    alice

ldap group을 vault policy에 맵핑해봅니다.

# root 계정으로 다시 로그인(alice 계정 그대로 사용할 경우)
vault login

# 정책 생성 
vault policy write admin - <<EOF
path "*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
EOF

# LDAP admins 그룹에 admin (vault) 정책 지정
vault write auth/ldap/groups/admins policies=admin

ldap 구조를 보면 alice는 admin, bob은 devs 그룹 유저입니다. 정책은 admin만 생성했기에 admin만 리소스가 보이고 devs는 아무런 리소스가 보여야하지 않습니다.

dc=example,dc=org
├── ou=people
│   ├── uid=alice
│   │   ├── cn: Alice
│   │   ├── sn: Kim
│   │   ├── uid: alice
│   │   └── mail: alice@example.org
│   └── uid=bob
│       ├── cn: Bob
│       ├── sn: Lee
│       ├── uid: bob
│       └── mail: bob@example.org
└── ou=groups
    ├── cn=devs
    │   └── member: uid=bob,ou=people,dc=example,dc=org
    └── cn=admins
        └── member: uid=alice,ou=people,dc=example,dc=org

alice와 bob으로 각각 로그인을 해서 확인해봅니다.

# alice 로그인 : 토큰, policy 확인
vault login -method=ldap username=alice password=alice123
...
token_policies         ["admin" "default"]
identity_policies      []
policies               ["admin" "default"]
...

# 정책 적용 확인
vault policy read admin 
path "*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}

# bob으로 로그인
vault login -method=ldap username=bob password=bob123
vault policy read admin

Error reading policy named admin: Error making API request.

URL: GET http://localhost:30000/v1/sys/policies/acl/admin
Code: 403. Errors:

* 1 error occurred:
	* permission denied

bob은 권한이 없음을 알 수 있습니다.

위와 같이 ldap과 vault를 연결하여 그룹기반으로 권한제어를 할 수 있습니다.

'스터디 > [gasida] ci-cd 스터디 1기' 카테고리의 다른 글

Helm, Tekton  (0) 2025.12.05
Image Build  (0) 2025.12.05
Vault  (0) 2025.11.26
OpenLDAP + KeyCloak + Argo CD + Jenkins  (0) 2025.11.23
ArgoCD ApplicationSet  (0) 2025.11.23

+ Recent posts