본문 바로가기
CI&CD

[Vault] VSO(Vault Secrets Operator)로 Kubernetes Secret 안전하게 관리하기

by interlude-3 2025. 12. 13.

Vault 목표

Kubernetes 환경에서 애플리케이션이 필요로 하는 Secret 정보를 코드 또는 이미지에 하드코딩하지 않고,
ServiceAccount 기반으로 인증된 신원만 HashiCorp Vault를 통해 접근하도록 통제하는 것을 목표로 합니다.

Vault 기반 Secret 관리 기본 동작

1. Vault 사전 설정

  • Kubernetes 인증을 위해 Vault에 Kubernetes Auth를 활성화하고, ServiceAccount와 Namespace를 기준으로 접근 가능한 Secret 범위를 Policy 및 Role로 정의

2. Pod 생성 및 ServiceAccount JWT 발급

  • Pod가 생성되면 Kubernetes는 ServiceAccount를 바인딩하고, Vault 인증에 사용될 ServiceAccount JWT 토큰을 Pod 내부에 자동으로 마운트

3. 애플리케이션의 Vault 직접 인증

  • Pod 내 애플리케이션은 ServiceAccount JWT를 사용하여 Vault Kubernetes Auth 엔드포인트로 직접 인증 요청 수행
  • Vault는 Kubernetes TokenReview API를 통해 Pod의 신원 검증

4. Vault 토큰 발급 및 Secret 조회

  • 인증이 완료되면 Vault는 Auth Token을 발급하고, 애플리케이션은 해당 토큰의 권한 범위 내에서 필요한 Secret 조회

 

VSO 도입

 

기존 방식의 문제점

 

Kubernetes 환경에서 Vault를 직접 사용하는 방식은 애플리케이션 Pod가 스스로 Vault에 접속하여 Secret을 가져오는 구조입니다. 이 경우 애플리케이션은 본연의 비즈니스 로직뿐만 아니라 다음과 같은 보안 관련 작업도 함께 처리해야 합니다.

  • Vault SDK를 사용한 인증 로직 구현
  • 인증 토큰 관리 및 갱신
  • 토큰 만료 시 재인증 처리

그러다 보면 애플리케이션 코드에 비즈니스 로직과 보안 로직이 섞이게 되고, 프로그래밍 언어나 팀마다 Vault 연동 방식이 달라질 수 있습니다. 또한 Secret이 변경되거나 Vault 정책이 업데이트될 때마다 각 애플리케이션이 개별적으로 대응해야 하므로, 서비스가 많아질수록 관리가 복잡해지고 일관성을 유지하기 어려워집니다.

 

VSO를 통한 개선

 

Vault Secrets Operator(VSO)는 Vault 연동 책임을 애플리케이션에서 분리하여 전담 Operator에게 위임합니다.

 

VSO 도입 후 역할 분담

  • VSO: Vault 인증, Secret 조회, Kubernetes Secret 생성 및 동기화 담당
  • 애플리케이션: Vault를 직접 알 필요 없이 일반적인 Kubernetes Secret만 참조

이를 통해 애플리케이션 코드는 비즈니스 로직에만 집중할 수 있고, Vault 관련 로직은 VSO가 중앙에서 일관되게 처리합니다. VSO는 Vault의 보안 모델을 그대로 유지하면서도, Secret 관리를 표준화하고 운영 복잡도를 낮추는 솔루션입니다.

 

VSO 기반 Secret 관리

1. (초기 세팅) Kubernetes Auth 준비 + VSO 설치
2. (Vault 관리 대상 / App 세팅) 엔진 설정 + Policy / Role 준비
3. (런타임 동작) VSO - VaultAuth로 로그인
4. (런타임 동작) VSO - VaultSecret / VaultStaticSecret / VaultDynamicSecret 리소스를 통한 Secret 동기화
5. (App 동작) Pod는 K8s Secret만 사용

[초기 세팅]
- Kubernetes Auth
- VSO 설치

[백엔드 리소스 설정] : Vault가 무엇을 관리할지
- "Secret Engine" enable (KV or Database or PKI)
** Database 엔진인 경우 **
DB config (Vault → DB 접속 정보)
DB role (계정 생성/회수 규칙)

[애플리케이션 설정] : 누가 어디까지 쓸 수 있는지
- policy
- k8s auth role
- VaultAuth
- VaultStaticSecret / VaultDynamicSecret / VaultPKISecret
- ServiceAccount

 

VSO 실습 시나리오

1. Static Secret (VaultStaticSecret CRD)
2. Dynamic Secret (VaultDynamicSecret CRD)
3. PKI Certificate (Vault PKI)
 
1. Static Secret
Static Secret은 값이 고정되어 있으며, 주로 DB 비밀번호, API Key, 토큰과 같은 형태의 Secret에 사용됩니다.
Vault에는 KV 엔진에 정적 Secret이 저장되어 있고, VSO는 VaultStaticSecret 리소스를 통해 해당 Secret을 주기적으로 조회하여 Kubernetes Secret으로 동기화합니다. 이 과정에서 Vault 인증은 VSO가 수행하며, 애플리케이션은 Vault의 존재를 인식하지 않고 단순히 Kubernetes Secret을 참조합니다.
 
2. Dynamic Secret
Dynamic Secret은 요청 시점에 Vault가 동적으로 생성하며, 일정 시간이 지나면 자동으로 만료(lease)되는 Secret입니다.
대표적인 예로는 데이터베이스 계정 정보가 있으며, Vault는 요청이 들어올 때마다 새로운 계정을 발급하고 정해진 TTL 이후 자동으로 폐기합니다. VSO는 VaultDynamicSecret 리소스를 통해 Dynamic Secret을 요청하고, 해당 Secret을 Kubernetes Secret으로 동기화합니다. 또한 Secret의 lease 갱신이나 재발급 과정 역시 애플리케이션이 아닌 VSO가 담당합니다.
 
3. PKI 기반 인증서 발급
Vault는 PKI 엔진을 통해 인증서를 발급할 수 있으며, Kubernetes 환경에서는 두 가지 방식으로 활용할 수 있습니다.
1) cert-manager와 Vault PKI 연동        2) VSO의 VaultPKISecret CRD 사용
본 실습에서는 cert manager를 사용하여 Vault PKI 엔진을 CA로 연동하고, 인증서를 Kubernetes Secret으로 자동 관리하는 과정을 실습합니다. Vault PKI 엔진은 요청이 올 때마다 새로운 인증서(공개 키와 개인 키)를 동적으로 생성하며, 자체적으로 CA 역할을 수행하거나 또는 상위 CA의 서명을 받아 인증서를 발급합니다. cert manager는 Certificate라는 Kubernetes 리소스를 감시하면서 리소스에 정의된 대로 인증 기관(CA)에 인증서를 요청하고, 발급받은 인증서를 Kubernetes Secret으로 저장하며, 만료일이 다가오면 자동으로 갱신하는 모든 작업을 처리합니다.

 

  엔진 종류 시크릿 종류 역할 및 특징 결과물 (Secret)
실습 1 KV (Key/Value) 정적 (Static) 정적인 데이터(비밀번호, API 키 등)를 안전하게 저장하고 관리합니다. 가장 기본이 되는 엔진입니다. 정해진 값의 API 키, DB 암호, 설정 값
실습 2 Database 동적 (Dynamic) 요청 시 DB(PostgreSQL, MySQL, MongoDB 등)에 접속할 수 있는 임시 사용자 계정을 생성하고, 사용 후 자동으로 폐기합니다. 임시 DB 사용자 이름 및 암호
실습 3 PKI (Public Key Infrastructure) 동적 (Dynamic) X.509 인증서 및 개인 키를 요청 시 동적으로 생성하고, 지정된 기간 후 자동으로 만료/폐기합니다. X.509 인증서, 개인 키 (TLS/SSL)
  AWS 동적 (Dynamic) AWS API 접근을 위한 임시 IAM 자격 증명 (Access Key 및 Secret Key)을 생성하고, 만료되면 자동으로 폐기합니다. 임시 AWS 액세스 키 쌍
  Azure/GCP 동적 (Dynamic) 클라우드 플랫폼(Azure/GCP)의 API 접근을 위한 임시 서비스 계정 또는 토큰을 생성하고 관리합니다. 임시 Azure/GCP 자격 증명
  SSH 동적 (Dynamic) SSH 연결을 위해 임시 SSH 키 쌍을 발급하거나, 기존 사용자에게 임시적인 권한을 부여하는 인증서를 생성합니다. 임시 SSH 키 쌍 또는 인증서
  Transit 암호화/복호화 시크릿을 저장하지 않고, Vault를 암호화 서비스로 활용합니다. 애플리케이션 데이터를 Vault로 보내 암호화한 후 다시 돌려받아 다른 저장소에 저장합니다. 암호화된 데이터 (Data Encryption

실습 진행

1. 기본 환경 셋팅

1-1) kind 클러스터 설치

# myk8s 클러스터 생성
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'

 
1-2) Vault 설치 (dev mode)

(⎈|kind-myk8s:N/A) root@DESKTOP-HUU6SC7:~/ria/vault/learn-vault-secrets-operator# helm repo add hashicorp https://helm.releases.hashicorp.com
"hashicorp" has been added to your repositories

(⎈|kind-myk8s:N/A) root@DESKTOP-HUU6SC7:~/ria/vault/learn-vault-secrets-operator# helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "externaldns" chart repository
...Successfully got an update from the "external-dns" chart repository
...Successfully got an update from the "hashicorp" chart repository
...Successfully got an update from the "bitnami" chart repository
Update Complete. ⎈Happy Helming!⎈

(⎈|kind-myk8s:N/A) root@DESKTOP-HUU6SC7:~/ria/vault/learn-vault-secrets-operator# 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.1.0           1.1.0           Official Vault Secrets Operator Chart
# Clone the repository
git clone https://github.com/hashicorp-education/learn-vault-secrets-operator
cd learn-vault-secrets-operator

# 테스트 용도(server.dev.enabled=true) 설정 파일 작성
# dev mode는 직접 Unseal 하지 않아도됨. 
# RootToken은 원래는 랜덤생성되고 로그에서 찾아야하지만 이번엔 root로 직접 설정
cat <<EOF > vault-values.yaml
server:
  image:
    repository: "hashicorp/vault"
    tag: "1.19.0"

  dev:
    enabled: true
    devRootToken: "root"

  logLevel: debug

  service:
    enabled: true
    type: ClusterIP
    port: 8200
    targetPort: 8200

ui:
  enabled: true
  serviceType: "NodePort"
  externalPort: 8200
  serviceNodePort: 30000

injector:
  enabled: "false"
EOF

# vault 설치
helm install vault hashicorp/vault -n vault --create-namespace --values vault-values.yaml --version 0.30.0

(⎈|kind-myk8s:N/A) root@DESKTOP-HUU6SC7:~/ria/vault/learn-vault-secrets-operator# kubectl get pods -n vault
NAME      READY   STATUS    RESTARTS   AGE
vault-0   1/1     Running   0          2m23s

 
1-3) VSO 실습 전 Vault 사전 설정

  • VSO와 애플리케이션이 사용할 Secret Engine / Policy / Auth Role을 미리 준비
# vault cli 설치 전 필수 패키지 설치
sudo apt-get update
sudo apt-get install -y gpg curl lsb-release

# HashiCorp 공식 APT Repository 등록
# HashiCorp GPG Key 등록
curl -fsSL https://apt.releases.hashicorp.com/gpg \
  | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg

# HashiCorp Repo 추가
echo "deb [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
  
# Vault 설치
sudo apt-get update
sudo apt-get install -y vault

# 설치 확인
vault --version

# Vault 주소 설정 (NodePort 30000 사용)
export VAULT_ADDR='http://localhost:30000'

(⎈|kind-myk8s:N/A) root@DESKTOP-HUU6SC7:~/ria/vault/learn-vault-secrets-operator# vault status
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    1
Threshold       1
Version         1.19.0
Build Date      2025-03-04T12:36:40Z
Storage Type    inmem
Cluster Name    vault-cluster-9b9dff19
Cluster ID      cf405792-33f2-081f-fd65-866a1cfd780f
HA Enabled      false
(⎈|kind-myk8s:N/A) root@DESKTOP-HUU6SC7:~/ria/vault/learn-vault-secrets-operator# vault login
Token (will be hidden): 
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                root
token_accessor       QNO5aO59u44ioYKvIek7zpHi
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]
#vault 인증 활성화
# vault auth enable -path demo-auth-mount kubernetes
Success! Enabled kubernetes auth method at: demo-auth-mount/

#Vault가 어느 Kubernetes 클러스터를 신뢰할지 k8s API 등록
# vault write auth/demo-auth-mount/config kubernetes_host="https://kubernetes.default.svc"
Success! Data written to: auth/demo-auth-mount/config

#KV v2 Secret Engine 활성화
# vault secrets enable -path=kvv2 kv-v2
Success! Enabled the kv-v2 secrets engine at: kvv2/

#webapp Policy 생성 - 어떤 path를 어떤 동작으로 접근할 수 있는지
tee webapp.json <<EOF
"kvv2/d> path "kvv2/data/webapp/config" {
>    capabilities = ["read", "list"]
> }
> EOF
path "kvv2/data/webapp/config" {
   capabilities = ["read", "list"]
}
# vault policy write webapp webapp.json
Success! Uploaded policy: webapp

#Role 생성 - 신분+권한 매핑
#app 네임스페이스의 demo-static-app ServiceAccount만 webapp정책으로 로그인 가능
# 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
Success! Data written to: auth/demo-auth-mount/role/role1

 
1-4) VSO(Vault Secrets Operator) 설치

# 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.1.0           1.1.0           Official Vault Secrets Operator Chart

# cat vault/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: demo-auth-mount
        keyName: vso-client-cache
        transitMount: demo-transit
        kubernetes:
          role: auth-role-operator
          serviceAccount: vault-secrets-operator-controller-manager
          tokenAudiences: ["vault"]

# helm install vault-secrets-operator hashicorp/vault-secrets-operator -n vault-secrets-operator-system --create-namespace --values vault/vault-operator-values.yaml --version 0.7.1
NAME: vault-secrets-operator
LAST DEPLOYED: Sun Dec 14 21:19:51 2025
NAMESPACE: vault-secrets-operator-system
STATUS: deployed
REVISION: 1
# kubectl get crd | grep secrets.hashicorp.com
hcpauths.secrets.hashicorp.com                2025-12-14T12:19:47Z
hcpvaultsecretsapps.secrets.hashicorp.com     2025-12-14T12:19:48Z
secrettransformations.secrets.hashicorp.com   2025-12-14T12:19:48Z
vaultauths.secrets.hashicorp.com              2025-12-14T12:19:48Z
vaultconnections.secrets.hashicorp.com        2025-12-14T12:19:48Z
vaultdynamicsecrets.secrets.hashicorp.com     2025-12-14T12:19:49Z
vaultpkisecrets.secrets.hashicorp.com         2025-12-14T12:19:50Z
vaultstaticsecrets.secrets.hashicorp.com      2025-12-14T12:19:50Z

# kubectl describe pod -n vault-secrets-operator-system | grep Image
    Image:         gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0
    Image ID:      gcr.io/kubebuilder/kube-rbac-proxy@sha256:d8cc6ffb98190e8dd403bfe67ddcb454e6127d32b87acc237b3e5240f70a20fb
    Image:         hashicorp/vault-secrets-operator:0.7.1
    Image ID:      docker.io/hashicorp/vault-secrets-operator@sha256:0e985a12d76b776b86194daeb6911cd8bd74aadb9df86db7ec442fbb2368b97f
  Normal  Pulled     75s   kubelet            Successfully pulled image "gcr.io/kubebuilder/kube-rbac-proxy:v0.15.0" in 8.851s (8.851s including waiting). Image size: 25163193 bytes.
  Normal  Pulled     63s   kubelet            Successfully pulled image "hashicorp/vault-secrets-operator:0.7.1" in 12.098s (12.098s including waiting). Image size: 33783530 bytes.

2. VaultStaticSecret을 활용한 정적 Secret 동기화

2-1) Vault Policy/Role 생성 (Vault 사전 설정에서 완료)
2-2) 실습용 Vault Static Secret 생성

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

======= Metadata =======
Key                Value
---                -----
created_time       2025-12-13T01:50:13.20371792Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

 
2-3) VaultAuth 생성

  • app 네임스페이스의 demo-static-app ServiceAccount로 Vault의 demo-auth-mount / role1을 사용해서 로그인
# kubectl create ns app

# cat vault/vault-auth-static.yaml
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
      
# kubectl apply -f vault/vault-auth-static.yaml
serviceaccount/demo-operator created
serviceaccount/demo-static-app created
vaultauth.secrets.hashicorp.com/static-auth created

# kubectl get sa,vaultauth -n app
NAME                             SECRETS   AGE
serviceaccount/default           0         6m51s
serviceaccount/demo-static-app   0         5s

NAME                                          AGE
vaultauth.secrets.hashicorp.com/static-auth   5s

 
2-4) VaultStaticSecret 리소스 생성

  • VSO는 VaultAuth 리소스에 정의된 ServiceAccount 정보를 사용해 Kubernetes Auth 방식으로 Vault에 인증
# cat vault/static-secret.yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
  name: vault-kv-app
  namespace: app
spec:
  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
  
# kubectl apply -f vault/static-secret.yaml
vaultstaticsecret.secrets.hashicorp.com/vault-kv-app created

# kubectl get vaultstaticsecret -n app
NAME           AGE
vault-kv-app   11s

 
2-5) Kubernetes Secret 동기화 확인

# kubectl get secret -n app
NAME       TYPE     DATA   AGE
secretkv   Opaque   3      6s

# kubectl get secret secretkv -n app -o json | jq -r '.data | to_entries[] | "\(.key)=\(.value | @base64d)"'
password=static-password
username=static-user

# 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-14T12:45:40.11923092Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            2

# kubectl get secret secretkv -n app -o json | jq -r '.data | to_entries[] | "\(.key)=\(.value | @base64d)"'
password=static-password2
username=static-user2

3. VaultDynamicSecret을 활용한 동적 Secret 관리

3-1) 테스트용 PostgreSQL 파드 배포

# kubectl create ns postgres
namespace/postgres created

# helm repo add bitnami https://charts.bitnami.com/bitnami
"bitnami" already exists with the same configuration, skipping

# helm upgrade --install postgres bitnami/postgresql --namespace postgres --set auth.audit.logConnections=true  --set auth.postgresPassword=secret-pass
Release "postgres" does not exist. Installing it now.
NAME: postgres
LAST DEPLOYED: Sun Dec 14 21:52:11 2025
NAMESPACE: postgres
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
CHART NAME: postgresql
CHART VERSION: 18.1.13
APP VERSION: 18.1.0
# kubectl get secret postgres-postgresql -n postgres -o json | jq -r '.data | to_entries[] | "\(.key)=\(.value | @base64d)"'
postgres-password=secret-pass

# kubectl exec -it -n postgres postgres-postgresql-0 -- sh -c "PGPASSWORD=secret-pass psql -U postgres -h localhost -c '\l'"
                                                     List of databases
   Name    |  Owner   | Encoding | Locale Provider |   Collate   |    Ctype    | Locale | ICU Rules |   Access privileges
-----------+----------+----------+-----------------+-------------+-------------+--------+-----------+-----------------------
 postgres  | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |        |           |
 template0 | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |        |           | =c/postgres          +
           |          |          |                 |             |             |        |           | postgres=CTc/postgres
 template1 | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |        |           | =c/postgres          +
           |          |          |                 |             |             |        |           | postgres=CTc/postgres
(3 rows)

 
3-2) Database Secret Engine 활성화

# vault secrets enable -path=demo-db database
Success! Enabled the database secrets engine at: demo-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"
Success! Data written to: demo-db/config/demo-db

# 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"
Success! Data written to: demo-db/roles/dev-postgres

# vault policy write demo-auth-policy-db - <<EOF
h "demo-db/cred> path "demo-db/creds/dev-postgres" {
>    capabilities = ["read"]
> }
> EOF
Success! Uploaded policy: demo-auth-policy-db

# 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


3-3) Dynamic Secret 접근을 위한 Policy/Role 생성

# vault write auth/demo-auth-mount/role/auth-role \
>    bound_service_account_names=demo-dynamic-app \
>    bound_service_account_namespaces=demo-ns \
0 \
   t>    token_ttl=0 \
>    token_period=120 \
ken_>    token_policies=demo-auth-policy-db \
>    audience=vault
Success! Data written to: auth/demo-auth-mount/role/auth-role

# vault read auth/demo-auth-mount/role/auth-role
Key                                         Value
---                                         -----
alias_name_source                           serviceaccount_uid
audience                                    vault
bound_service_account_names                 [demo-dynamic-app]
bound_service_account_namespace_selector    n/a
bound_service_account_namespaces            [demo-ns]
token_bound_cidrs                           []
token_explicit_max_ttl                      0s
token_max_ttl                               0s
token_no_default_policy                     false
token_num_uses                              0
token_period                                2m
token_policies                              [demo-auth-policy-db]
token_ttl                                   0s
token_type                                  default

 
3-4) VaultAuth 생성 & VaultDynamicSecret 리소스 생성

# kubectl create ns demo-ns
namespace/demo-ns created

# kubectl apply -f dynamic-secrets/.
deployment.apps/vso-db-demo created
secret/vso-db-demo created
serviceaccount/demo-dynamic-app created
vaultauth.secrets.hashicorp.com/dynamic-auth created
vaultdynamicsecret.secrets.hashicorp.com/vso-db-demo-create created
vaultdynamicsecret.secrets.hashicorp.com/vso-db-demo created
serviceaccount/demo-operator unchanged

# kubectl get pod -n demo-ns
NAME                           READY   STATUS    RESTARTS   AGE
vso-db-demo-7f5f487775-spbkc   1/1     Running   0          42s
vso-db-demo-7f5f487775-th4qh   1/1     Running   0          42s
vso-db-demo-7f5f487775-wjc7l   1/1     Running   0          16s
# kubectl exec -it deploy/vso-db-demo -n demo-ns -- ls -al /etc/secrets
total 8
drwxrwxrwt 3 root root  140 Dec 14 12:57 .
drwxr-xr-x 1 root root 4096 Dec 14 12:58 ..
drwxr-xr-x 2 root root  100 Dec 14 12:57 ..2025_12_14_12_57_47.3956952921
lrwxrwxrwx 1 root root   32 Dec 14 12:57 ..data -> ..2025_12_14_12_57_47.3956952921
lrwxrwxrwx 1 root root   11 Dec 14 12:57 _raw -> ..data/_raw
lrwxrwxrwx 1 root root   15 Dec 14 12:57 password -> ..data/password
lrwxrwxrwx 1 root root   15 Dec 14 12:57 username -> ..data/username

# kubectl exec -it deploy/vso-db-demo -n demo-ns -- cat /etc/secrets/username ; echo
v-demo-aut-dev-post-auh6c6E9GYxzuD49ksfZ-1765717067

# kubectl exec -it deploy/vso-db-demo -n demo-ns -- cat /etc/secrets/password ; echo
b1-f9OymF5u-lC4pIyWD

# kubectl get secret -n demo-ns
NAME                  TYPE     DATA   AGE
vso-db-demo           Opaque   3      71s
vso-db-demo-created   Opaque   3      71s

 
3-5) Dynamic Secret 자동 갱신 확인
// 자동 갱신 전 Secret 값

# 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-KMH06GkIFopLl1KvbbwB-1765717067 | Password valid until 2025-12-14 13:07:52+00
 v-demo-aut-dev-post-Xgxy8C8aZiHD30SDUF8G-1765717067 | Password valid until 2025-12-14 13:07:52+00
 v-demo-aut-dev-post-auh6c6E9GYxzuD49ksfZ-1765717067 | Password valid until 2025-12-14 13:07:52+00
 v-demo-aut-dev-post-kIa65jaXnIUfbHUDiR9v-1765717067 | Password valid until 2025-12-14 13:07:52+00

# kubectl get secret -n demo-ns
NAME                  TYPE     DATA   AGE
vso-db-demo           Opaque   3      4m30s
vso-db-demo-created   Opaque   3      4m30s

# kubectl get secret vso-db-demo -n demo-ns -o json | jq -r '.data | to_entries[] | "\(.key)=\(.value | @base64d)"'
_raw={"password":"b1-f9OymF5u-lC4pIyWD","username":"v-demo-aut-dev-post-auh6c6E9GYxzuD49ksfZ-1765717067"}
password=b1-f9OymF5u-lC4pIyWD
username=v-demo-aut-dev-post-auh6c6E9GYxzuD49ksfZ-1765717067

 
// 약 10분 후 자동 갱신 결과

# kubectl get pod -n demo-ns
NAME                          READY   STATUS    RESTARTS   AGE 
vso-db-demo-9f9869cd9-6lhxl   1/1     Running   0          108s
vso-db-demo-9f9869cd9-7f7lq   1/1     Running   0          108s
vso-db-demo-9f9869cd9-9jvpl   1/1     Running   0          105s

# 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-KMH06GkIFopLl1KvbbwB-1765717067 | Password valid until 2025-12-14 13:07:52+00
 v-demo-aut-dev-post-Xgxy8C8aZiHD30SDUF8G-1765717067 | Password valid until 2025-12-14 13:07:52+00
 v-demo-aut-dev-post-YN5Kxj1t6Y4GYubtgvPF-1765717956 | Password valid until 2025-12-14 13:22:41+00
 v-demo-aut-dev-post-auh6c6E9GYxzuD49ksfZ-1765717067 | Password valid until 2025-12-14 13:17:52+00
 v-demo-aut-dev-post-hX3QYrXLV0A5qQ6p7KP7-1765717909 | Password valid until 2025-12-14 13:21:54+00
 v-demo-aut-dev-post-kIa65jaXnIUfbHUDiR9v-1765717067 | Password valid until 2025-12-14 13:17:52+00
 
 # kubectl get secret vso-db-demo -n demo-ns -o json | jq -r '.data | to_entries[] | "\(.key)=\(.value | @base64d)"'
_raw={"password":"toLck0cYroCnSRJ9g-yd","username":"v-demo-aut-dev-post-hX3QYrXLV0A5qQ6p7KP7-1765717909"}
password=toLck0cYroCnSRJ9g-yd
username=v-demo-aut-dev-post-hX3QYrXLV0A5qQ6p7KP7-1765717909

 
// 특정 lease 삭제해보기

  • Lease = Vault가 발급한 동적 자격증명의 유효기간 + 관리 단위
  • Vault가 동적으로 생성한 Secret(DB 계정, 토큰 등)에 대해 유효기간(TTL)과 회수(revoke) / 갱신(renew)을 관리하기 위해 붙이는 ID
# vault list sys/leases/lookup/demo-db/creds/dev-postgres
Keys
----
NtIFN6n4clCFsaFlQmaW8Q1g
Y9jYHfTUltwX3dvgSswmiXc8

# vault lease revoke demo-db/creds/dev-postgres/Y9jYHfTUltwX3dvgSswmiXc8
All revocation operations queued successfully!

# vault list sys/leases/lookup/demo-db/creds/dev-postgres
Keys
----
NtIFN6n4clCFsaFlQmaW8Q1g

# 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-GZCXWwXcl2Z4yKzaHfx5-1765718780 | Password valid until 2025-12-14 13:43:42+00
 v-demo-aut-dev-post-KMH06GkIFopLl1KvbbwB-1765717067 | Password valid until 2025-12-14 13:07:52+00
 v-demo-aut-dev-post-UqK9iR767BptTpatN9JV-1765718807 | Password valid until 2025-12-14 13:44:15+00
 v-demo-aut-dev-post-Xgxy8C8aZiHD30SDUF8G-1765717067 | Password valid until 2025-12-14 13:07:52+00
 v-demo-aut-dev-post-YN5Kxj1t6Y4GYubtgvPF-1765717956 | Password valid until 2025-12-14 13:32:41+00
 v-demo-aut-dev-post-auh6c6E9GYxzuD49ksfZ-1765717067 | Password valid until 2025-12-14 13:17:52+00
 v-demo-aut-dev-post-hX3QYrXLV0A5qQ6p7KP7-1765717909 | Password valid until 2025-12-14 13:31:54+00
 v-demo-aut-dev-post-kIa65jaXnIUfbHUDiR9v-1765717067 | Password valid until 2025-12-14 13:17:52+00

# vault read demo-db/creds/dev-postgres
Key                Value
---                -----
lease_id           demo-db/creds/dev-postgres/GSudrdaCs0qcPIVTToStbA4v
lease_duration     10m
lease_renewable    true
password           B-RjxP8xMnqu5TDl1GPM
username           v-token-dev-post-FkV2AioMIVSxjcqwK3W6-1765719411

# 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-GZCXWwXcl2Z4yKzaHfx5-1765718780 | Password valid until 2025-12-14 13:43:42+00
 v-demo-aut-dev-post-KMH06GkIFopLl1KvbbwB-1765717067 | Password valid until 2025-12-14 13:07:52+00
 v-demo-aut-dev-post-UqK9iR767BptTpatN9JV-1765718807 | Password valid until 2025-12-14 13:44:15+00
 v-demo-aut-dev-post-Xgxy8C8aZiHD30SDUF8G-1765717067 | Password valid until 2025-12-14 13:07:52+00
 v-demo-aut-dev-post-YN5Kxj1t6Y4GYubtgvPF-1765717956 | Password valid until 2025-12-14 13:32:41+00
 v-demo-aut-dev-post-auh6c6E9GYxzuD49ksfZ-1765717067 | Password valid until 2025-12-14 13:17:52+00
 v-demo-aut-dev-post-hX3QYrXLV0A5qQ6p7KP7-1765717909 | Password valid until 2025-12-14 13:31:54+00
 v-demo-aut-dev-post-kIa65jaXnIUfbHUDiR9v-1765717067 | Password valid until 2025-12-14 13:17:52+00
 v-token-dev-post-FkV2AioMIVSxjcqwK3W6-1765719411    | Password valid until 2025-12-14 13:46:56+00

# kubectl logs -n vault -l app.kubernetes.io/name=vault --follow
2025-12-14T13:34:52.463Z [DEBUG] secrets.pki.pki_0b44d0fa: starting to process revocation requests
2025-12-14T13:34:52.463Z [DEBUG] secrets.pki.pki_0b44d0fa: gathered 0 revocations and 0 confirmation entries
2025-12-14T13:34:52.463Z [DEBUG] secrets.pki.pki_0b44d0fa: starting to process unified revocations
2025-12-14T13:35:52.552Z [DEBUG] secrets.pki.pki_0b44d0fa: starting to process revocation requests
2025-12-14T13:35:52.552Z [DEBUG] secrets.pki.pki_0b44d0fa: gathered 0 revocations and 0 confirmation entries
2025-12-14T13:35:52.552Z [DEBUG] secrets.pki.pki_0b44d0fa: starting to process unified revocations
2025-12-14T13:36:16.406Z [INFO]  expiration: revoked lease: lease_id=demo-db/creds/dev-postgres/Y9jYHfTUltwX3dvgSswmiXc8

 

4. Vault PKI와 Cert-manager 연동을 통한 TLS 인증서 자동 관리 (VSO ❌)

4-1) Vault PKI 엔진 활성화 및 CA 구성

# vault secrets enable pki
Success! Enabled the pki secrets engine at: pki/

# vault secrets tune -max-lease-ttl=8760h pki
Success! Tuned the secrets engine at: pki/

# vault write pki/root/generate/internal \
>     common_name=example.com \
ttl=8760>     ttl=8760h
WARNING! The following warnings were returned from Vault:

  * This mount hasn't configured any authority information access (AIA)
  fields; this may make it harder for systems to find missing certificates
  in the chain or to validate revocation status of certificates. Consider
  updating /config/urls or the newly generated issuer with this information.

Key              Value
---              -----
certificate      -----BEGIN CERTIFICATE-----
MIIDNTCCAh2gAwIBAgIUR2I4GXNTDyfn2mUJdQsz59EyRnIwDQYJKoZIhvcNAQEL
BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMjUxMjE0MTMwNzQ4WhcNMjYx
MjE0MTMwODE4WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAOdpcxm9UdGIhqkTeTsknMxVxRseN0FkvVy+8AZA
klJjCTEG9TT0+YrugWR7PdjHlU6Y8nSzeXh0zY6YB9+2epJVmUPrjPhMdXR5pyU7
LeGP3kIGZwiGVcRGj+7Ul9CeAb63Ot6h/71KsZ6LrvxZtAcdYjgVyLF2mL/92347
t33K8OOfrIwQ0LmLnmRoh/nKf6SzOMhBpwLWN4VklIw3xPPQnyC8Y/2NFUy9Gfb9
SdG+rYFYvAiLpvbZ5lFEAXCBp1wfbZAuUA08r8O5zS+YJII0bFuZcYXTeKkIosR0
dCoPCFNwsrqSQqd6pMqVEUMtSOuIFFPdk4PBrkcV4Ql6rtkCAwEAAaN7MHkwDgYD
VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBPeHtKQANxr
yP2eehQr9+IZEt+LMB8GA1UdIwQYMBaAFBPeHtKQANxryP2eehQr9+IZEt+LMBYG
A1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQB85l+fgC0N
gmvdUBqopjAITNKwIS7TJrA++5wX0Y/agWkYz8qvwacHAitfUhZPWy1FyG/5xLls
XGWDjeHTIT7uPTcn04B1UMJFolBkO6NUMfrBv9DHKrlL3YQVFu8xTx/HAgXjkEQw
YziU5JvQQg4d68BqPGTf88u3rFdJefIBMvtEJR2D1AEazOYhSnBB9mmS4rxcb6ns
cTE88zMo7oCeZFJnfG1WY7FDCIvExryljOnz50PUb0ZSi8wdGi+tTOZNG7at1cgK
3rVh0Pn0zWqM1fyNN3lajdx8eumtB1JbWrp/E024sJNNf/u7kmh0bgQzHqC/5uDl
pwzffPvru+r9
-----END CERTIFICATE-----
expiration       1797253698
issuer_id        51273b8f-5e9e-e08b-949e-581dc323ce2a
issuer_name      n/a
issuing_ca       -----BEGIN CERTIFICATE-----
MIIDNTCCAh2gAwIBAgIUR2I4GXNTDyfn2mUJdQsz59EyRnIwDQYJKoZIhvcNAQEL
BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMjUxMjE0MTMwNzQ4WhcNMjYx
MjE0MTMwODE4WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN
AQEBBQADggEPADCCAQoCggEBAOdpcxm9UdGIhqkTeTsknMxVxRseN0FkvVy+8AZA
klJjCTEG9TT0+YrugWR7PdjHlU6Y8nSzeXh0zY6YB9+2epJVmUPrjPhMdXR5pyU7
LeGP3kIGZwiGVcRGj+7Ul9CeAb63Ot6h/71KsZ6LrvxZtAcdYjgVyLF2mL/92347
t33K8OOfrIwQ0LmLnmRoh/nKf6SzOMhBpwLWN4VklIw3xPPQnyC8Y/2NFUy9Gfb9
SdG+rYFYvAiLpvbZ5lFEAXCBp1wfbZAuUA08r8O5zS+YJII0bFuZcYXTeKkIosR0
dCoPCFNwsrqSQqd6pMqVEUMtSOuIFFPdk4PBrkcV4Ql6rtkCAwEAAaN7MHkwDgYD
VR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFBPeHtKQANxr
yP2eehQr9+IZEt+LMB8GA1UdIwQYMBaAFBPeHtKQANxryP2eehQr9+IZEt+LMBYG
A1UdEQQPMA2CC2V4YW1wbGUuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQB85l+fgC0N
gmvdUBqopjAITNKwIS7TJrA++5wX0Y/agWkYz8qvwacHAitfUhZPWy1FyG/5xLls
XGWDjeHTIT7uPTcn04B1UMJFolBkO6NUMfrBv9DHKrlL3YQVFu8xTx/HAgXjkEQw
YziU5JvQQg4d68BqPGTf88u3rFdJefIBMvtEJR2D1AEazOYhSnBB9mmS4rxcb6ns
cTE88zMo7oCeZFJnfG1WY7FDCIvExryljOnz50PUb0ZSi8wdGi+tTOZNG7at1cgK
3rVh0Pn0zWqM1fyNN3lajdx8eumtB1JbWrp/E024sJNNf/u7kmh0bgQzHqC/5uDl
pwzffPvru+r9
-----END CERTIFICATE-----
key_id           457a2460-adc0-53c8-777b-a2733872d721
key_name         n/a
serial_number    47:62:38:19:73:53:0f:27:e7:da:65:09:75:0b:33:e7:d1:32:46:72

# vault read pki/config/urls
Key                        Value
---                        -----
crl_distribution_points    [http://vault.vault.svc:8200/v1/pki/crl]
enable_templating          false
issuing_certificates       [http://vault.vault.svc:8200/v1/pki/ca]
ocsp_servers               []


4-2) PKI 접근을 위한 Policy 및 Kubernetes Auth Role 생성

# vault write pki/roles/example-dot-com \
    >     allowed_domains=example.com \
>     allow_subdomains=true \
>     max_ttl=72h
Key                                   Value
---                                   -----
allow_any_name                        false
allow_bare_domains                    false
allow_glob_domains                    false
allow_ip_sans                         true
allow_localhost                       true
allow_subdomains                      true
allow_token_displayname               false
allow_wildcard_certificates           true
allowed_domains                       [example.com]
allowed_domains_template              false
allowed_other_sans                    []
allowed_serial_numbers                []
allowed_uri_sans                      []
allowed_uri_sans_template             false
allowed_user_ids                      []
basic_constraints_valid_for_non_ca    false
client_flag                           true
cn_validations                        [email hostname]
code_signing_flag                     false
country                               []
email_protection_flag                 false
enforce_hostnames                     true
ext_key_usage                         []
ext_key_usage_oids                    []
generate_lease                        false
issuer_ref                            default
key_bits                              2048
key_type                              rsa
key_usage                             [DigitalSignature KeyAgreement KeyEncipherment]
locality                              []
max_ttl                               72h
no_store                              false
not_after                             n/a
not_before_duration                   30s
organization                          []
ou                                    []
policy_identifiers                    []
postal_code                           []
province                              []
require_cn                            true
serial_number_source                  json-csr
server_flag                           true
signature_bits                        256
street_address                        []
ttl                                   0s
use_csr_common_name                   true
use_csr_sans                          true
use_pss                               false

# vault policy write pki - <<EOF
 "pki*> path "pki*"                        { capabilities = ["read", "list"] }
> path "pki/sign/example-dot-com"    { capabilities = ["create", "update"] }
/issue/e> path "pki/issue/example-dot-com"   { capabilities = ["create"] }
> EOF
Success! Uploaded policy: pki
# vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/

# vault write auth/kubernetes/config kubernetes_host="https://kubernetes.default.svc"
Success! Data written to: auth/kubernetes/config

# vault write auth/kubernetes/role/issuer \
>     bound_service_account_names=issuer \
>     bound_service_account_namespaces=default \
>     policies=pki \
>     ttl=20m
Success! Data written to: auth/kubernetes/role/issuer


4-3) cert-manager 설치 (vso 대신 cert manager 사용)

# kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.12.3/cert-manager.crds.yaml
customresourcedefinition.apiextensions.k8s.io/certificaterequests.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/certificates.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/challenges.acme.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/clusterissuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/issuers.cert-manager.io created
customresourcedefinition.apiextensions.k8s.io/orders.acme.cert-manager.io created

# kubectl get crd | grep cert-manager
certificaterequests.cert-manager.io           2025-12-14T13:16:11Z
certificates.cert-manager.io                  2025-12-14T13:16:11Z
challenges.acme.cert-manager.io               2025-12-14T13:16:11Z
clusterissuers.cert-manager.io                2025-12-14T13:16:11Z
issuers.cert-manager.io                       2025-12-14T13:16:11Z
orders.acme.cert-manager.io                   2025-12-14T13:16:11Z

# kubectl create namespace cert-manager
namespace/cert-manager created

# helm repo add jetstack https://charts.jetstack.io
"jetstack" has been added to your repositories

# helm repo update
Update Complete. ⎈Happy Helming!⎈

# helm install cert-manager --namespace cert-manager --version v1.12.3 jetstack/cert-manager
NAME: cert-manager
LAST DEPLOYED: Sun Dec 14 22:16:31 2025
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
cert-manager v1.12.3 has been deployed successfully!

In order to begin issuing certificates, you will need to set up a ClusterIssuer
or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer).

More information on the different types of issuers and how to configure them
can be found in our documentation:

https://cert-manager.io/docs/configuration/

For information on how to configure cert-manager to automatically provision
Certificates for Ingress resources, take a look at the `ingress-shim`
documentation:

https://cert-manager.io/docs/usage/ingress/

# kubectl get pods --namespace cert-manager
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-cainjector-74bf899bd6-2d694   1/1     Running   0          44s
cert-manager-fb6f6945f-sqfgn               1/1     Running   0          44s
cert-manager-webhook-8fc69bc68-bcfh2       1/1     Running   0          44s

 
4-4) Vault 기반 Issuer 리소스 생성

# kubectl create serviceaccount issuer
serviceaccount/issuer created

# cat >> issuer-secret.yaml <<EOF
> apiVersion: v1
> kind: Secret
> metadata:
>   name: issuer-token-lmzpj
>   annotations:
>     kubernetes.io/service-account.name: issuer
type: kubernetes.io/service-account> type: kubernetes.io/service-account-token
> EOF

# kubectl apply -f issuer-secret.yaml
secret/issuer-token-lmzpj created

# kubectl get secrets
NAME                 TYPE                                  DATA   AGE
issuer-token-lmzpj   kubernetes.io/service-account-token   3      3s

# ISSUER_SECRET_REF=$(kubectl get secrets --output=json | jq -r '.items[].metadata | select(.name|startswith("issuer-token-")).name')

# echo $ISSUER_SECRET_REF
issuer-token-lmzpj
# cat > vault-issuer.yaml <<EOF
> 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

# kubectl apply --filename vault-issuer.yaml
issuer.cert-manager.io/vault-issuer created


4-5) TLS 인증서 Kubernetes Secret 동기화 확인

# cat > example-com-cert.yaml <<EOF
> apiVersion: cert-manager.io/v1
> kind: Certificate
ta:
 > metadata:
>   name: example-com
>   namespace: default
> spec:
>   secretName: $ISSUER_SECRET_REF
>   issuerRef:
>     name: vault-issuer
>   commonName: www.example.com
>   dnsNames:
>   - www.example.com
> EOF

# kubectl apply --filename example-com-cert.yaml
certificate.cert-manager.io/example-com created

# kubectl get certificate.cert-manager.io/example-com -owide
NAME          READY   SECRET               ISSUER         STATUS                                          AGE
example-com   True    issuer-token-lmzpj   vault-issuer   Certificate is up to date and has not expired   3s
# kubectl describe certificate.cert-manager example-com
Name:         example-com
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  cert-manager.io/v1
Kind:         Certificate
Metadata:
  Creation Timestamp:  2025-12-14T13:24:54Z
  Generation:          1
  Resource Version:    15073
  UID:                 71cbde64-01de-46a2-929b-28cf8c0ea462
Spec:
  Common Name:  www.example.com
  Dns Names:
    www.example.com
  Issuer Ref:
    Name:       vault-issuer
  Secret Name:  issuer-token-lmzpj
Status:
  Conditions:
    Last Transition Time:  2025-12-14T13:24:55Z
    Message:               Certificate is up to date and has not expired
    Observed Generation:   1
    Reason:                Ready
    Status:                True
    Type:                  Ready
  Not After:               2025-12-17T13:24:55Z
  Not Before:              2025-12-14T13:24:25Z
  Renewal Time:            2025-12-16T13:24:45Z
  Revision:                1
Events:
  Type    Reason     Age   From                                       Message
  ----    ------     ----  ----                                       -------
  Normal  Issuing    53s   cert-manager-certificates-trigger          Issuing certificate as Secret does not contain a private key
  Normal  Generated  53s   cert-manager-certificates-key-manager      Stored new private key in temporary Secret resource "example-com-j42sg"
  Normal  Requested  53s   cert-manager-certificates-request-manager  Created new CertificateRequest resource "example-com-mwwt9"
  Normal  Issuing    53s   cert-manager-certificates-issuing          The certificate has been successfully issued
# kubectl get certificaterequests.cert-manager.io -owide
NAME                APPROVED   DENIED   READY   ISSUER         REQUESTOR                                         STATUS                                         AGE
example-com-mwwt9   True                True    vault-issuer   system:serviceaccount:cert-manager:cert-manager   Certificate fetched from issuer successfully   84s