cloudNet@ 팀의 가시다 님이 진행하는 쿠버네티스 CI/CD 스터디 8주차 내용입니다.
Vault 설치 on K8S
참고 : [Tutorials] Vault on K8S https://developer.hashicorp.com/vault/tutorials/kubernetes
1. K8S (kind) 설치
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 ps
kubectl get node
NAME STATUS ROLES AGE VERSION
myk8s-control-plane Ready control-plane 4h12m v1.32.2
# 노드에 기본 툴 설치
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'
2. Vault 설치 - Docs , Helm
# Setup Helm repo
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
helm search repo hashicorp/vault
# Create a Kubernetes namespace.
kubectl create namespace vault
cat <<EOF > vault-values.yaml
global:
enabled: true
tlsDisable: true
server:
standalone:
enabled: true
config: |
ui = true
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_disable = 1
}
storage "file" {
path = "/vault/data"
}
dataStorage:
enabled: true
size: "10Gi"
mountPath: "/vault/data"
auditStorage:
enabled: true
size: "10Gi"
mountPath: "/vault/logs"
service:
enabled: true
type: NodePort
nodePort: 30000
ui:
enabled: true
injector:
enabled: false
EOF
# helm 설치
helm upgrade vault hashicorp/vault -n vault -f vault-values.yaml --install --dry-run=client
helm upgrade vault hashicorp/vault -n vault -f vault-values.yaml --install --version 0.31.0
# 배포확인 : vault-0 파드는 기동 시 Readiness Probe 체크 실패 상태
## (참고) Readiness: exec [/bin/sh -ec vault status -tls-skip-verify]
kubectl get-all -n vault
kubectl get sts,pods,svc,ep,pvc,cm -n vault
...
NAME READY STATUS RESTARTS AGE
pod/vault-0 0/1 Running 0 35s
...
# Vault Status 명령으로 Sealed 상태확인
kubectl exec -ti vault-0 -n vault -- vault status
...
Key Value
--- -----
Seal Type shamir
Initialized false
Sealed true
...
# vault 로그 확인
kubectl stern -n vault -l app.kubernetes.io/name=vault
...
vault-0 vault 2025-04-16T05:35:09.225Z [INFO] core: seal configuration missing, not initialized
...
3. Vault Unseal - Docs

# Initialize vault-0 with one key share and one key threshold.
kubectl exec vault-0 -n vault -- vault operator init \
-key-shares=1 \
-key-threshold=1 \
-format=json > cluster-keys.json
# cluster-keys.json 파일 확인
cat cluster-keys.json| jq
{
"unseal_keys_b64": [
"VthosBSlObBJ1DSpenVby4wmxt+Dx2dWeldBK725ies="
],
...
"root_token": "hvs.XEPc44aa6q5CFA8puMfSIwlo"
}
# Display the unseal key found in cluster-keys.json.
jq -r ".unseal_keys_b64[]" cluster-keys.json
VthosBSlObBJ1DSpenVby4wmxt+Dx2dWeldBK725ies=
# Create a variable named VAULT_UNSEAL_KEY to capture the Vault unseal key.
VAULT_UNSEAL_KEY=$(jq -r ".unseal_keys_b64[]" cluster-keys.json)
# Unseal Vault running on the vault-0 pod : The Vault server is initialized and unsealed.
kubectl exec vault-0 -n vault -- vault operator unseal $VAULT_UNSEAL_KEY
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
...
# vault-0 파드 확인 : Readiness Probe 체크 성공!
## (참고) Readiness: exec [/bin/sh -ec vault status -tls-skip-verify]
kubectl get pod -n vault
NAME READY STATUS RESTARTS AGE
vault-0 1/1 Running 0 9m3s
# Display the root token found in cluster-keys.json.
jq -r ".root_token" cluster-keys.json
hvs.XEPc44aa6q5CFA8puMfSIwlo


4. Vault login with CLI
# 설치 (macOS)
brew tap hashicorp/tap
brew install hashicorp/tap/vault
vault --version # 설치 확인
# NodePort로 공개한 30000 NodePort로 설정
export VAULT_ADDR='http://localhost:30000'
# vault 상태확인
vault status
# Root Token으로 로그인
vault login
Token (will be hidden): hvs.XEPc44aa6q5CFA8puMfSIwlo
...
- (옵션) Vault UI 접속 : 로그인 Method(Token) , Token(위 Root Token 입력) 후 Sign in
# Vault Service(NodePort) 확인
kubectl get svc,ep -n vault vault
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/vault NodePort 10.96.171.218 <none> 8200:30000/TCP,8201:30334/TCP 23m
NAME ENDPOINTS AGE
endpoints/vault 10.244.0.20:8201,10.244.0.20:8200 23m
# Vault UI 접속
open http://127.0.0.1:30000
- (옵션) Vault Audit log : file 설정 - Link
- Vault Audit devices는 최소 2개 이상을 활성화 하는 것을 권장 : 예) File 과 Syslog , File 과 Socket 등등 - Docs
- PVC 디스크 풀 차면 Vault Audit 로그만 동작하지 않는게 아니라 Vault 자체 동작 수행을 막습니다. (일반적인 Log와는 다르다는 점!)
# audit 용 pvc 확인 : /vault/logs 마운트 설정되어 있음
kubectl get pvc -n vault
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
audit-vault-0 Bound pvc-51681977-020b-4838-9510-2d4a31748839 10Gi RWO standard <unset> 2m55s
data-vault-0 Bound pvc-ed68083a-e725-4243-b93e-3475a88bc1e6 10Gi RWO standard <unset> 2m55s
# audit 용 pv(pvc) 에 저장될 수 있게 file audit log 설정
vault audit enable file file_path=/vault/logs/audit.log
vault audit list -detailed
# 확인
kubectl exec -it vault-0 -n vault -- tail -f /vault/logs/audit.log
...
Vault 사용 on K8S
1. Vault 에 시크릿 생성 및 애플리케이션에서 시크릿 가져와보기

(1) Vault 에 Secret 를 요청 처리를 위해 사전에 Role(Policy) 설정
(2) 파드 생성 시, 서비스 어카운트 토큰(JWT) 생성
(3) 파드의 애플리케이션이 Vault 에 로그인 과정
- 애플리케이션은 JWT를 전달하여 Vault 로그인 요청
- Vault 는 정보 확인을 위해 K8S API 서버에 TokenReview API 호출
- K8S API 서버는 서비스 어카운트의 이름과 네임스페이스를 반환
- Vault 는 ‘서비스 어카운트 이름, 네임스페이스’를 Vault 해당 시크릿에 정책과 매칭 확인
- 확인 후 Vault 는 Auth Token 을 애플리케이션에게 반환
(4) 파드의 애플리케이션이 Vault 에 Secret 요청 과정
- 애플리케이션은 (3)에서 받은 Auth Token 으로 Vault 해당 시크릿 정보를 요청
- Vault 는 Auth Token 확인 및 매칭 정책 확인
- 확인 후 Vault 는 최종적으로 해당 시크릿 정보를 반환
Vault 에서 k8s-auth 인증은 아래 VSO에서도 활용됨. 참고로 AWS EKS에 aws-auth 인증/인가 시에도 유사한 과정을 사용. 🪕[NEW] AWS EKS Access Mgmt & Pod Identity
2. Set a secret in Vault - Link
# Enable an instance of the kv-v2 secrets engine at the path secret.
vault secrets enable -path=secret kv-v2
Success! Enabled the kv-v2 secrets engine at: secret/
# 확인
vault secrets list -detailed
vault secrets list
# Create a secret at path secret/webapp/config with a username and password.
vault kv put secret/webapp/config username="static-user" password="static-password"
====== Secret Path ======
secret/data/webapp/config
======= Metadata =======
Key Value
--- -----
created_time 2025-04-15T12:50:53.565975884Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
# Verify that the secret is defined at the path secret/webapp/config.
vault kv get secret/webapp/config
====== Secret Path ======
secret/data/webapp/config
======= Metadata =======
Key Value
--- -----
created_time 2025-04-15T12:50:53.565975884Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
====== Data ======
Key Value
--- -----
password static-password
username static-user
# Verify that the secret is defined at the path secret/webapp/config.
export VAULT_ROOT_TOKEN=hvs.nEMhDcK5ry90YR0sMXwD14bA
curl -s --header "X-Vault-Token: $VAULT_ROOT_TOKEN" --request GET \
http://127.0.0.1:30000/v1/secret/data/webapp/config | jq
...

3. Configure K8S Authentication in Vault - Link
Vault 설정 관계도 : [Auth] k8s policy(webapp) ← [Policy] path (secret/data/webapp/config ‘read’) ← [Secret] username , password
- 볼트는 고객이 Kubernetes 서비스 계정 토큰으로 인증할 수 있는 방법을 제공합니다.
- 볼트는 Kubernetes 클러스터 내의 모든 클라이언트로부터 이 서비스 토큰을 받습니다.
- 인증 중에 볼트는 구성된 Kubernetes 엔드포인트를 조회하여 서비스 계정 토큰이 유효한지 확인합니다.
# vault 서버가 가지고 있는 Role 확인 : 이를 통해 K8S Service Account Token 유효 여부 확인
## subjectaccessreviews : 쿠버네티스 환경에서 사용자 또는 그룹의 액션 수행 가능 여부 확인
## tokenreviews : 쿠버네티스 API 서버가 제시된 토큰의 유효성을 확인하고, 그 토큰과 관련된 사용자 정보를 얻기 위해 사용
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
...
• [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 [*] [-] [-] ✖ ✖ ✖ ✔ ✖ ✖ ✖ ✖
# Enable the Kubernetes authentication method.
vault auth enable kubernetes
Success! Enabled kubernetes auth method at: kubernetes/
# 확인
vault auth list -detailed
vault auth list
# K8S API 서버 정보 설정 : 현재 vault 가 k8s 에 설치되어 있으므로, 아래처럼 서비스명 주소 입력 가능
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc"
Success! Data written to: auth/kubernetes/config
# 설정 정보 확인
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
use_annotations_as_alias_metadata false
- 클라이언트가 secret/webapp/config에서 정의된 비밀 데이터에 접근하려면, path secret/data/webapp/config에 대한 읽기 기능이 부여되어야 합니다.
- 이것은 정책의 한 예입니다. 정책은 일련의 기능을 정의합니다.
# Write out the policy named webapp that enables the read capability for secrets at path secret/data/webapp/config.
vault policy write webapp - <<EOF
path "secret/data/webapp/config" {
capabilities = ["read"]
}
EOF
Success! Uploaded policy: webapp
- webapp policy 을 사용하는 인증 메서드 역할을 정의합니다.
- 역할은 정책과 환경 매개변수를 결합하여 웹 애플리케이션의 로그인을 생성합니다.
# Kubernetes 서비스 계정 이름과 웹앱 정책을 연결하는 웹앱이라는 이름의 Kubernetes 인증 역할을 만듭니다.
# Create a Kubernetes authentication role, named webapp, that connects the Kubernetes service account name and webapp policy.
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

- 이 역할은 기본적으로 Kubernetes 서비스 계정, 볼트, 네임스페이스를 볼트 정책인 웹앱과 연결합니다. 인증 후 반환되는 토큰은 24시간 동안 유효합니다.
4. K8S 파드의 애플리케이션이 사용할 수 있는 인증 관련 정보 - Docs , Link

- 서비스 어카운트 Service Account
- 서비스어카운트(ServiceAccount) 는 파드에서 실행되는 애플리케이션 프로세스에 대한 식별자를 제공한다.
- 파드 내부의 애플리케이션 프로세스는, 자신에게 부여된 서비스 어카운트의 식별자를 사용하여 클러스터의 API 서버에 인증할 수 있다.
- 서비스 어카운트 토큰 serviceAccountToken
- 서비스어카운트토큰(serviceAccountToken) 정보는 kubelet이 kube-apiserver로부터 취득한 토큰을 포함한다.
- kubelet은 TokenRequest API를 통해 일정 시간 동안 사용할 수 있는 토큰을 발급 받는다.
- 이렇게 취득한 토큰은 파드가 삭제되거나 지정된 수명 주기 이후에 만료된다(기본값은 1시간이다).
- 이 토큰은 특정한 파드에 바인딩되며 kube-apiserver를 그 대상으로 한다.
- 토큰 컨트롤러 token Controller
- kube-controller-manager 의 일부로써 실행되며, 비동기적으로 동작한다.
- 서비스어카운트에 대한 삭제를 감시하고, 해당하는 모든 서비스어카운트 토큰 시크릿을 같이 삭제한다.
- 서비스어카운트 토큰 시크릿에 대한 추가를 감시하고, 참조된 서비스어카운트가 존재하는지 확인하며, 필요한 경우 시크릿에 토큰을 추가한다.
- 시크릿에 대한 삭제를 감시하고, 필요한 경우 해당 서비스어카운트에서 참조 중인 항목들을 제거한다.
- 서비스 어카운트 어드미션 컨트롤러 Service Account Admission Controller
- 파드에 .spce.serviceAccountName 항목이 지정되지 않았다면, 어드미션 컨트롤러는 실행하려는 파드의 서비스어카운트 이름을 default로 설정한다.
- 어드미션 컨트롤러는 실행되는 파드가 참조하는 서비스어카운트가 존재하는지 확인한다.
- 만약 해당하는 이름의 서비스어카운트가 존재하지 않는 경우, 어드미션 컨트롤러는 파드를 실행시키지 않는다.
- 이는 default 서비스어카운트에 대해서도 동일하게 적용된다.
- 서비스어카운트의 automountServiceAccountToken 또는 파드의 automountServiceAccountToken 중 어느 것도 false 로 설정되어 있지 않다면,
- 어드미션 컨트롤러는 실행하려는 파드에 API에 접근할 수 있는 토큰을 포함하는 볼륨 을 추가한다.
- 어드미션 컨트롤러는 파드의 각 컨테이너에 volumeMount를 추가한다.
- 이미 /var/run/secrets/kubernetes.io/serviceaccount 경로에 볼륨이 마운트 되어있는 컨테이너에 대해서는 추가하지 않는다.
- 리눅스 컨테이너의 경우, 해당 볼륨은 /var/run/secrets/kubernetes.io/serviceaccount 위치에 마운트된다
- 파드의 spec에 imagePullSecrets 이 없는 경우, 어드미션 컨트롤러는 ServiceAccount의 imagePullSecrets을 복사하여 추가된다.
- 어드미션 컨트롤러는 파드의 생성 시점에 다음 작업들을 수행한다.
- TokenRequest API
- 서비스어카운트의 하위 리소스인 TokenRequest를 사용하여 일정 시간 동안 해당 서비스어카운트에서 사용할 수 있는 토큰을 가져올 수 있다.
- 컨테이너 내에서 사용하기 위한 API 토큰을 얻기 위해 이 요청을 직접 호출할 필요는 없는데, kubelet이 프로젝티드 볼륨 을 사용하여 이를 설정하기 때문이다.
- 프로젝티드 볼륨 Projected Volumes - Docs
4. Service Account Token Volume Projection, Admission Control, JWT(JSON Web Token), OIDC
Service Account Token Volume Projection : '서비스 계정 토큰'의 시크릿 기반 볼륨 대신 'projected volume' 사용
- Service Account Token (SAT) Volume Projection - 링크

- 서비스 계정 토큰을 이용해서 서비스와 서비스, 즉 파드(pod)와 파드(pod)의 호출에서 자격 증명으로 사용할 수 있을까요?
- 불행히도 기본 서비스 계정 토큰으로는 사용하기에 부족함이 있습니다. 토큰을 사용하는 대상(audience), 유효 기간(expiration) 등 토큰의 속성을 지정할 필요가 있기 때문입니다.
- Service Account Token Volume Projection 기능을 사용하면 이러한 부족한 점들을 해결할 수 있습니다.
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- image: nginx
name: nginx
volumeMounts:
- mountPath: /var/run/secrets/tokens
name: vault-token
serviceAccountName: build-robot
volumes:
- name: vault-token
projected:
sources:
- serviceAccountToken:
path: vault-token
expirationSeconds: 7200
audience: vault
- Bound Service Account Token Volume 바인딩된 서비스 어카운트 토큰 볼륨 - 링크 영어
- FEATURE STATE: Kubernetes v1.22 [stable]
- 서비스 어카운트 어드미션 컨트롤러는 토큰 컨트롤러에서 생성한 만료되지 않은 서비스 계정 토큰에 시크릿 기반 볼륨 대신 다음과 같은 프로젝티드 볼륨을 추가한다.
- name: kube-api-access-<random-suffix>
projected:
defaultMode: 420 # 420은 rw- 로 소유자는 읽고쓰기 권한과 그룹내 사용자는 읽기만, 보통 0644는 소유자는 읽고쓰고실행 권한과 나머지는 읽고쓰기 권한
sources:
- serviceAccountToken:
expirationSeconds: 3607
path: token
- configMap:
items:
- key: ca.crt
path: ca.crt
name: kube-root-ca.crt
- downwardAPI:
items:
- fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
path: namespace
- 프로젝티드 볼륨은 세 가지로 구성된다. PSAT (Projected Service Account Tokens)

- kube-apiserver로부터 TokenRequest API를 통해 얻은 서비스어카운트토큰(ServiceAccountToken). 서비스어카운트토큰은 기본적으로 1시간 뒤에, 또는 파드가 삭제될 때 만료된다. 서비스어카운트토큰은 파드에 연결되며 kube-apiserver를 위해 존재한다.
- kube-apiserver에 대한 연결을 확인하는 데 사용되는 CA 번들을 포함하는 컨피그맵(ConfigMap).
- 파드의 네임스페이스를 참조하는 DownwardA
- Configure a Pod to Use a Projected Volume for Storage : 시크릿 컨피그맵 downwardAPI serviceAccountToken의 볼륨 마운트를 하나의 디렉터리에 통합 - 링크
- This page shows how to use a projected Volume to mount several existing volume sources into the same directory. Currently, secret, configMap, downwardAPI, and serviceAccountToken volumes can be projected.
- Note: serviceAccountToken is not a volume type.
apiVersion: v1
kind: Pod
metadata:
name: test-projected-volume
spec:
containers:
- name: test-projected-volume
image: busybox:1.28
args:
- sleep
- "86400"
volumeMounts:
- name: all-in-one
mountPath: "/projected-volume"
readOnly: true
volumes:
- name: all-in-one
projected:
sources:
- secret:
name: user
- secret:
name: pass
# Create the Secrets:
## Create files containing the username and password:
echo -n "admin" > ./username.txt
echo -n "1f2d1e2e67df" > ./password.txt
## Package these files into secrets:
kubectl create secret generic user --from-file=./username.txt
kubectl create secret generic pass --from-file=./password.txt
# 파드 생성
kubectl apply -f https://k8s.io/examples/pods/storage/projected.yaml
# 파드 확인
kubectl get pod test-projected-volume -o yaml | kubectl neat
...
volumes:
- name: all-in-one
projected:
defaultMode: 420
sources:
- secret:
name: user
- secret:
name: pass
- name: kube-api-access-n6n9v
projected:
defaultMode: 420
sources:
- serviceAccountToken:
expirationSeconds: 3607
path: token
- configMap:
items:
- key: ca.crt
path: ca.crt
name: kube-root-ca.crt
- downwardAPI:
items:
- fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
path: namespace
# 시크릿 확인
kubectl exec -it test-projected-volume -- ls /projected-volume/
password.txt username.txt
kubectl exec -it test-projected-volume -- cat /projected-volume/username.txt ;echo
admin
kubectl exec -it test-projected-volume -- cat /projected-volume/password.txt ;echo
1f2d1e2e67df
# 삭제
kubectl delete pod test-projected-volume && kubectl delete secret user pass
5. k8s api 접근 단계
- AuthN → AuthZ → Admisstion Control 권한이 있는 사용자에 한해서 관리자(Admin)가 특정 행동을 제한(validate) 혹은 변경(mutate) - 링크 Slack
- AuthN & AuthZ - MutatingWebhook - Object schema validation - ValidatingWebhook → etcd

- Admission Control도 Webhook으로 사용자에게 API가 열려있고, 사용자는 자신만의 Admission Controller를 구현할 수 있으며, 이를 Dynamic Admission Controller라고 부르고, 크게 MutatingWebhook 과 ValidatingWebhook 로 나뉩니다.
- MutatingWebhook은 사용자가 요청한 request에 대해서 관리자가 임의로 값을 변경하는 작업입니다.
- ValidatingWebhook은 사용자가 요청한 request에 대해서 관리자기 허용을 막는 작업입니다.
#
kubectl get mutatingwebhookconfigurations
NAME WEBHOOKS AGE
aws-load-balancer-webhook 3 98m
kube-prometheus-stack-admission 1 96m
pod-identity-webhook 1 175m
vpc-resource-mutating-webhook 1 175m
#
kubectl get validatingwebhookconfigurations
NAME WEBHOOKS AGE
aws-load-balancer-webhook 3 97m
kube-prometheus-stack-admission 1 96m
vpc-resource-validating-webhook 2 175m
6. JWT : Bearer type - JWT(JSON Web Token) X.509 Certificate의 lightweight JSON 버전
- Bearer type 경우, 서버에서 지정한 어떠한 문자열도 입력할 수 있습니다. 하지만 굉장히 허술한 느낌을 받습니다.
- 이를 보완하고자 쿠버네티스에서 Bearer 토큰을 전송할 때 주로 JWT (JSON Web Token) 토큰을 사용합니다.
- JWT는 X.509 Certificate와 마찬가지로 private key를 이용하여 토큰을 서명하고 public key를 이용하여 서명된 메세지를 검증합니다.
- 이러한 메커니즘을 통해 해당 토큰이 쿠버네티스를 통해 생성된 valid한 토큰임을 인증할 수 있습니다.
- X.509 Certificate의 lightweight JSON 버전이라고 생각하면 편리합니다.
- jwt는 JSON 형태로 토큰 형식을 정의한 스펙입니다. jwt는 쿠버네티스에서 뿐만 아니라 다양한 웹 사이트에서 인증, 권한 허가, 세션관리 등의 목적으로 사용합니다.
- Header: 토큰 형식와 암호화 알고리즘을 선언합니다.
- Payload: 전송하려는 데이터를 JSON 형식으로 기입합니다.
- Signature: Header와 Payload의 변조 가능성을 검증합니다.
- 각 파트는 base64 URL 인코딩이 되어서 .으로 합쳐지게 됩니다.

7. OIDC
OIDC : 사용자를 인증해 사용자에게 액세스 권한을 부여할 수 있게 해주는 프로토콜 ⇒ [커피고래]님 블로그 OpenID Connect - 링크
- OAuth 2.0 : 권한허가 처리 프로토콜, 다른 서비스에 접근할 수 있는 권한을 획득하거나 반대로 다른 서비스에게 권한을 부여할 수 있음 - 생활코딩
- 위임 권한 부여 Delegated Authorization, 사용자 인증 보다는 제한된 사람에게(혹은 시스템) 제한된 권한을 부여하는가, 예) 페이스북 posting 권한
- Access Token : 발급처(OAuth 2.0), 서버의 리소스 접근 권한
- OpenID : 비영리기관인 OpenID Foundation에서 추진하는 개방형 표준 및 분산 인증 Authentication 프로토콜, 사용자 인증 및 사용자 정보 제공(id token) - 링크
- ID Token : 발급처(OpenID Connect), 유저 프로필 정보 획득
- OIDC OpenID Connect = OpenID 인증 + OAuth2.0 인가, JSON 포맷을 이용한 RESful API 형식으로 인증 - 링크
- iss: 토큰 발행자
- sub: 사용자를 구분하기 위한 유니크한 구분자
- email: 사용자의 이메일
- iat: 토큰이 발행되는 시간을 Unix time으로 표기한 것
- exp: 토큰이 만료되는 시간을 Unix time으로 표기한 것
- aud: ID Token이 어떤 Client를 위해 발급된 것인지.
- IdP Open Identify Provider : 구글, 카카오와 같이 OpenID 서비스를 제공하는 신원 제공자.
- OpenID Connect에서 IdP의 역할을 OAuth가 수행 - 링크
- RP Relying Party : 사용자를 인증하기 위해 IdP에 의존하는 주체
8. Launch a web application* - Link , Docs , Tokens

- 웹 애플리케이션을 생성하여 DockerHub에 게시하고 기존 클러스터에서 애플리케이션을 실행할 Kubernetes 배포를 만들었습니다.
- 예시 웹 애플리케이션은 HTTP 요청을 청취하는 단일 기능을 수행합니다.
- 요청 시 Kubernetes 서비스 토큰을 읽고 볼트에 로그인한 다음 비밀을 요청합니다..
- (참고) main.go : Vault와 상호작용하는 HTTP 서버를 구현
package main
import (
"fmt"
"log"
"os"
"time"
"bytes"
"net/http"
"io/ioutil"
"encoding/json"
)
func main() {
vaultToken := "root" # Vault 인증 토큰으로, 기본값은 "root"
port := os.Getenv("SERVICE_PORT") # port: 서버가 수신 대기할 포트. SERVICE_PORT 환경 변수에서 가져오며, 없으면 "8080"으로 기본 설정.
if port == "" {
port = "8080"
log.Println("PORT environment variable not set, defaulting to", port)
}
vaultUrl := os.Getenv("VAULT_ADDR") # vaultUrl: Vault 서버 주소. VAULT_ADDR 환경 변수에서 가져오며, 없으면 "http://vault:8200"으로 설정.
if vaultUrl == "" {
vaultUrl = "http://vault:8200"
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { # 루트 경로(/)로 들어오는 HTTP 요청을 처리하는 핸들러.
log.Println("Received Request - Port forwarding is working.")
// If the JWT path is setup then get the new token from Vault using the k8s Auth # K8s Auth를 사용하여 Vault에서 새 토큰을 가져옴.
jwtPath := os.Getenv("JWT_PATH")
if jwtPath != "" {
jwtFile, err := ioutil.ReadFile(jwtPath) # 지정된 경로에서 JWT 파일을 읽음.
if err != nil {
fmt.Println("Error reading JWT file at", jwtPath, ": ", err)
return
}
jwt := string(jwtFile)
fmt.Println("Read JWT:", jwt) # jwt 로그에 출력
authPath := "auth/kubernetes/login"
// Create the payload for Vault authentication #
pl := VaultJWTPayload { Role: "webapp", JWT: jwt }
jwtPayload, err := json.Marshal(pl)
if err != nil {
fmt.Println("Error encoding Vault request JSON:", err)
return
}
// Send a request to Vault to retrieve a token # Vault의 auth/kubernetes/login 엔드포인트로 JWT와 역할(webapp)을 포함한 요청을 보내 새로운 Vault 토큰을 획득.
vaultLoginResponse := &VaultLoginResponse{}
err = SendRequest(vaultUrl + "/v1/" + authPath, "", "POST", jwtPayload, vaultLoginResponse)
if err != nil {
fmt.Println("Error getting response from Vault k8s login:", err)
return
}
vaultToken = vaultLoginResponse.Auth.ClientToken
fmt.Println("Retrieved token: ", vaultToken)# 새로운 토큰을 로그에 출력
}
secretsPath := "secret/data/webapp/config"
// Send a request to Vault using the token to retrieve the secret # Vault의 secret/data/webapp/config 경로에서 시크릿을 요청.
vaultSecretResponse := &VaultSecretResponse{}
err := SendRequest(vaultUrl + "/v1/" + secretsPath, vaultToken, "GET", nil, &vaultSecretResponse)
if err != nil {
fmt.Println("Error getting secret from Vault:", err)
return
}
secretResponseData, ok := vaultSecretResponse.Data.Data.(map[string]interface{})
if ok {
for key, value := range secretResponseData { # 성공 시 시크릿 데이터를 map[string]interface{}로 변환해 키-값 쌍을 HTTP 응답으로 반환.
fmt.Fprintf(w, "%s:%s ", key, value)
}
} else {
fmt.Println("Error getting the secret from Vault, cannot convert Data to map[string]interface{}")
}
})
log.Println("Listening on port", port)
if err := http.ListenAndServe(":" + port, nil); err != nil {
log.Fatalf("Failed to start server:", err)
}
}
func SendRequest(url string, token string, requestType string, payload []byte, target interface{}) error { # Vault와의 HTTP 통신을 처리하는 유틸리티 함수.
req, err := http.NewRequest(requestType, url, bytes.NewBuffer(payload))
if err != nil {
fmt.Println("Error creating request:", err)
return err
}
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("X-Vault-Token", token) # HTTP 요청을 생성하고, 필요 시 X-Vault-Token 헤더 추가.
}
client := &http.Client{Timeout: 10 * time.Second} # 10초 타임아웃이 설정된 HTTP 클라이언트로 요청 전송.
res, err := client.Do(req)
if err != nil {
fmt.Println("Error sending request to Vault:", err)
return err
}
defer res.Body.Close()
return json.NewDecoder(res.Body).Decode(target) # 응답을 JSON으로 디코딩해 target에 저장.
- (참고) types.go : Vault와 상호작용하기 위한 데이터 구조체를 정의
package main
type VaultJWTPayload struct { # Vault의 Kubernetes 인증 방식(auth/kubernetes/login)에 필요한 요청 페이로드를 정의
Role string `json:"role"`
JWT string `json:"jwt"`
}
type VaultLoginResponse struct { # Vault의 Kubernetes 인증 요청(/v1/auth/kubernetes/login)에 대한 응답을 구조화
RequestID string `json:"request_id"`
LeaseID string `json:"lease_id"`
Renewable bool `json:"renewable"`
LeaseDuration int `json:"lease_duration"`
Data interface{} `json:"data"`
WrapInfo interface{} `json:"wrap_info"`
Warnings interface{} `json:"warnings"`
Auth struct {
ClientToken string `json:"client_token"`
Accessor string `json:"accessor"`
Policies []string `json:"policies"`
TokenPolicies []string `json:"token_policies"`
Metadata struct {
Role string `json:"role"`
ServiceAccountName string `json:"service_account_name"`
ServiceAccountNamespace string `json:"service_account_namespace"`
ServiceAccountSecretName string `json:"service_account_secret_name"`
ServiceAccountUID string `json:"service_account_uid"`
} `json:"metadata"`
LeaseDuration int `json:"lease_duration"`
Renewable bool `json:"renewable"`
EntityID string `json:"entity_id"`
TokenType string `json:"token_type"`
Orphan bool `json:"orphan"`
MfaRequirement interface{} `json:"mfa_requirement"`
NumUses int `json:"num_uses"`
} `json:"auth"`
}
type VaultSecretResponse struct { # Vault의 시크릿 조회 요청(예: /v1/secret/data/webapp/config)에 대한 응답을 구조화
RequestID string `json:"request_id"`
LeaseID string `json:"lease_id"`
Renewable bool `json:"renewable"`
LeaseDuration int `json:"lease_duration"`
Data struct {
Data interface{} `json:"data"`
} `json:"data"`
Warnings interface{} `json:"warnings"`
Auth interface{} `json:"auth"`
# vault 서비스 어카운트 생성
kubectl create sa vault
# 웹 애플리케이션 디플로이먼트 + 서비스(NodePort) 배포
# JWT_PATH sets the path of the JSON web token (JWT) issued by Kubernetes. This token is used by the web application to authenticate with Vault.
# VAULT_ADDR sets the address of the Vault service. The Helm chart defined a Kubernetes service named vault that forwards requests to its endpoints (i.e. The pods named vault-0, vault-1, and vault-2).
# SERVICE_PORT sets the port that the service listens for incoming HTTP requests.
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
NAME READY STATUS RESTARTS AGE
webapp-8d4859ff-9rvqt 1/1 Running 0 88s
# (참고) 코드 정보 확인
kubectl exec -it deploy/webapp -- cat /app/main.go
kubectl exec -it deploy/webapp -- cat /app/types.go
# 서비스 어카운트 토큰 확인 : 600초(10분)마다 갱신됨
kubectl exec -it deploy/webapp -- cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsImtpZCI6IlZIVlVTc21yMTBDZlhoNzEwb3dNVk5PYms1S0o1OHVoTlh5R0M1YjdiMjgifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzc2MjU5MTg5LCJpYXQiOjE3NDQ3MjMxODksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiOTg1ODc0MTUtZmJiNi00NmNmLThlN2YtZTIzZjk0NGQyNTU1Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwibm9kZSI6eyJuYW1lIjoibXlrOHMtY29udHJvbC1wbGFuZSIsInVpZCI6ImEzYTIzOWUyLTNkMzctNDU5ZS04OTg1LWUyMzcwZTliNWI3YSJ9LCJwb2QiOnsibmFtZSI6IndlYmFwcC04ZDQ4NTlmZi05cnZxdCIsInVpZCI6IjczNTlhZTdiLTY2MjYtNGYwOS1hNGFiLTY1MDc4YjY0OWYwNCJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoidmF1bHQiLCJ1aWQiOiJjOWNjYTA0ZS0yMDAwLTQ1YWQtODkxYi1kZDRhZGRiOTdmYWUifSwid2FybmFmdGVyIjoxNzQ0NzI2Nzk2fSwibmJmIjoxNzQ0NzIzMTg5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDp2YXVsdCJ9.Nxf0ka_gwzPzo6ISNnpY4u73LIjpyvmSLWWY2jj3EDiBtUFZyada6XTfSCwQSg8py3dikhtQAgaNG_feQsfycS0Xc3MJG88e_7R5I7fBVp9HXjhoFqyPl31isp-Gkbs8E6v4eD9caRzEoqrDeOpQcnNXKzfHx8O1LFUmtNGcNcJNv57WDFwyrLUwbLDBIZIfwtlsbX97nCl3LjcFfPwkrvV3rJw0gVInpblAmZQ8uZA6oRfkuA6B39sAPErxtnupdZMcnD4eKIxL2Ap3nE4O1VZ_2kJAMwwZkNGKV7TXvT1cjWqIH4Fn3WB--ZV_qvFNIH6EAc_LTO9rfQJpPvWXnA
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":1763822166,"iat":1763821566,"iss":"https://kubernetes.default.svc.cluster.local","jti":"c5333b9f-5470-4ff5-8375-c754665b81bd","kubernetes.io":{"namespace":"default","node":{"name":"myk8s-control-plane","uid":"8651b768-a9fa-40bf-9113-2d65db1e9477"},"pod":{"name":"webapp-9484c6fd7-x7kdl","uid":"b33edcaa-4ea8-4447-80e3-ff26cfce5ec1"},"serviceaccount":{"name":"vault","uid":"8b4051ca-ea3c-4db5-8499-3d7c97e560e4"}},"nbf":1763821566,"sub":"system:serviceaccount:default:vault"}
# 웹 애플리케이션 접속 동작 확인 : 접속 시 토큰 정보를 확인 후 vault 서버에 로그인 후 시크릿 정보 가져와서 http 출력
curl 127.0.0.1:30001
password:static-password username:static-user
# webapp 파드 로그 확인 : 서비스 어카운트 토큰(JWT) 정보 확인 후 vault 로그인 후 token 발급 확인
kubectl logs -l app=webapp -f
2025/04/15 13:26:42 Received Request - Port forwarding is working.
Read JWT: eyJhbGciOiJSUzI1NiIsImtpZCI6IlZIVlVTc21yMTBDZlhoNzEwb3dNVk5PYms1S0o1OHVoTlh5R0M1YjdiMjgifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzc2MjU5MTg5LCJpYXQiOjE3NDQ3MjMxODksImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiOTg1ODc0MTUtZmJiNi00NmNmLThlN2YtZTIzZjk0NGQyNTU1Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwibm9kZSI6eyJuYW1lIjoibXlrOHMtY29udHJvbC1wbGFuZSIsInVpZCI6ImEzYTIzOWUyLTNkMzctNDU5ZS04OTg1LWUyMzcwZTliNWI3YSJ9LCJwb2QiOnsibmFtZSI6IndlYmFwcC04ZDQ4NTlmZi05cnZxdCIsInVpZCI6IjczNTlhZTdiLTY2MjYtNGYwOS1hNGFiLTY1MDc4YjY0OWYwNCJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoidmF1bHQiLCJ1aWQiOiJjOWNjYTA0ZS0yMDAwLTQ1YWQtODkxYi1kZDRhZGRiOTdmYWUifSwid2FybmFmdGVyIjoxNzQ0NzI2Nzk2fSwibmJmIjoxNzQ0NzIzMTg5LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDp2YXVsdCJ9.Nxf0ka_gwzPzo6ISNnpY4u73LIjpyvmSLWWY2jj3EDiBtUFZyada6XTfSCwQSg8py3dikhtQAgaNG_feQsfycS0Xc3MJG88e_7R5I7fBVp9HXjhoFqyPl31isp-Gkbs8E6v4eD9caRzEoqrDeOpQcnNXKzfHx8O1LFUmtNGcNcJNv57WDFwyrLUwbLDBIZIfwtlsbX97nCl3LjcFfPwkrvV3rJw0gVInpblAmZQ8uZA6oRfkuA6B39sAPErxtnupdZMcnD4eKIxL2Ap3nE4O1VZ_2kJAMwwZkNGKV7TXvT1cjWqIH4Fn3WB--ZV_qvFNIH6EAc_LTO9rfQJpPvWXnA
Retrieved token: hvs.CAESIFUPqjOKrzUM78X7QQKX1H_88At5uyewGe-UT-lC56ERGh4KHGh2cy5JSk8xWkREM2lZRW9wclByNlA0T2FNTWU
...
- Vault 에서 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-04-15T15:14:59.37055476Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 2
# 확인
vault kv get secret/webapp/config
# 변경된 정보 확인
curl 127.0.0.1:30001
password:changed-password username:changed-user
- (참고) jwt.io : 서비스 어카운트 토큰(JWT) 정보 확인

- (심화) webapp → vault : vault 로그인 ⇒ Secret Read 요청 트래픽 확인
# 1. 노드 진입
docker exec -it myk8s-control-plane bash
# 2. 인터페이스 확인
ip link | grep ^10:
10: vethe73a7122@if2: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP mode DEFAULT group default qlen 1000
# 3. Vault 포트(8200)로 오고 가는 데이터를 실시간으로 캡처
root@myk8s-control-plane:/# ngrep -tW byline -d vethe73a7122 '' 'tcp port 8200'
# 4. 트래픽 생성
curl 127.0.0.1:30001
# 1-요청 : vault 인증
T 2025/12/11 01:39:56.553293 10.244.0.9:39520 -> 10.96.240.121:8200 [AP] #4
POST /v1/auth/kubernetes/login HTTP/1.1.
Host: vault.vault.svc:8200.
User-Agent: Go-http-client/1.1.
Content-Length: 1175.
Content-Type: application/json.
Accept-Encoding: gzip.
.
{"role":"webapp","jwt":"eyJhbGciOiJSUzI1NiIsImtpZCI6IkQ2YndCeWVvYTRLUWYzNnJWVDh2cl85bkNkdHBfcVJKVkFEWDBCcGJiaHMifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzY1NDE3Mjg0LCJpYXQiOjE3NjU0MTY2ODQsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiN2EwY2UzZWMtNjBkNi00YjU2LTg2OTYtNjdjY2I5ZmZlMTA1Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwibm9kZSI6eyJuYW1lIjoibXlrOHMtY29udHJvbC1wbGFuZSIsInVpZCI6IjY2Mzk5N2RjLTVlNzEtNGJkMS04YzU2LTEyYzBlNzI2ZjkxYyJ9LCJwb2QiOnsibmFtZSI6IndlYmFwcC05NDg0YzZmZDctNWd6bWIiLCJ1aWQiOiJiNTNmZjVmYy1jNWQ3LTQ1ZGYtYmQyYy0xNjBlZDA2Y2I1MWEifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6InZhdWx0IiwidWlkIjoiNTBjZDVjZjAtYWU0OC00Y2EyLTg1YmEtMjRiMzUyNDdmYTJhIn19LCJuYmYiOjE3NjU0MTY2ODQsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OnZhdWx0In0.E3Ij4zahYPN8QnObFeEcMeqURf--eeSTrbYn5cVHTvLKnmg-uIiLVieA-eqZU6fqX35HFNJRf4fs3Cj5ltvqoCUljiyn5SyWBKUoiNdWgyFga8yXSqq2JQ6rSi6xWnouqYy4y0G7RdpMqlEeUkjoHerHnHk-qZHv885To6N7BazImSorrxtTFyNKjgF2fCv1-Rv4XP3ULiiDqCzG1VqWGKy3SJaPoa5SnkOF6HmsI8tSku-JOlocNDPZbZTgUOojFfWLtYZI0xpKnDfLTaLe2J3qTzC0QXn-tlCQLds20PN1vjs96n-APMAWvWYKHPJ09TKaPakdIMtmXbVeKQBRPA"}
# 1-응답 : 토큰 받음
T 2025/12/11 01:39:56.572088 10.96.240.121:8200 -> 10.244.0.9:39520 [AP] #6
HTTP/1.1 200 OK.
Cache-Control: no-store.
Content-Type: application/json.
Strict-Transport-Security: max-age=31536000; includeSubDomains.
Date: Thu, 11 Dec 2025 01:39:56 GMT.
Content-Length: 752.
.
{"request_id":"a336c3c3-a5ce-36d3-b400-0c826d017291","lease_id":"","renewable":false,"lease_duration":0,"data":null,"wrap_info":null,"warnings":null,"auth":{"client_token":"hvs.CAESIMHwUr3M2UcDR_23877D-E-w4cLCNJE1NhdrMFB8nki5Gh4KHGh2cy5nSlE5ZWhxdFhmeE13U0NBa0FPRUZpazU","accessor":"pYxhVusjGzi88ayI1aQWsKMy","policies":["default","webapp"],"token_policies":["default","webapp"],"metadata":{"role":"webapp","service_account_name":"vault","service_account_namespace":"default","service_account_secret_name":"","service_account_uid":"50cd5cf0-ae48-4ca2-85ba-24b35247fa2a"},"lease_duration":86400,"renewable":true,"entity_id":"5154e8ab-f183-3a57-a96b-e02aa6b01071","token_type":"service","orphan":true,"mfa_requirement":null,"num_uses":0},"mount_type":""}
# 2-요청 : 발급 받은 토큰으로 secret 요청
T 2025/12/11 01:39:56.572752 10.244.0.9:39520 -> 10.96.240.121:8200 [AP] #8
GET /v1/secret/data/webapp/config HTTP/1.1.
Host: vault.vault.svc:8200.
User-Agent: Go-http-client/1.1.
Content-Type: application/json.
X-Vault-Token: hvs.CAESIMHwUr3M2UcDR_23877D-E-w4cLCNJE1NhdrMFB8nki5Gh4KHGh2cy5nSlE5ZWhxdFhmeE13U0NBa0FPRUZpazU.
Accept-Encoding: gzip.
.
# 2-응답 : secret 받음
T 2025/12/11 01:39:56.575013 10.96.240.121:8200 -> 10.244.0.9:39520 [AP] #9
HTTP/1.1 200 OK.
Cache-Control: no-store.
Content-Type: application/json.
Strict-Transport-Security: max-age=31536000; includeSubDomains.
Date: Thu, 11 Dec 2025 01:39:56 GMT.
Content-Length: 375.
.
{"request_id":"68ae5681-2bc4-7d52-861f-2fad39894693","lease_id":"","renewable":false,"lease_duration":0,"data":{"data":{"password":"changed-password","username":"changed-user"},"metadata":{"created_time":"2025-12-11T01:25:38.843861529Z","custom_metadata":null,"deletion_time":"","destroyed":false,"version":3}},"wrap_info":null,"warnings":null,"auth":null,"mount_type":"kv"}

- (심화) webapp → vault : audit 로그 확인
kubectl exec -it vault-0 -n vault -- tail -f /vault/logs/audit.log
- 1-요청 : vault 인증
{
"auth": {
"policy_results": {
"allowed": true
},
"token_type": "default"
},
"request": {
"data": {
"jwt": "hmac-sha256:ef7ef69ada78306a869b639647c1a4e647ab7cd448ed42137ef528a1c9244575",
"role": "hmac-sha256:00d7e66d7673210eeb1434c40d7a56b53d583857c1d1dd5fcb32a4abfb3ffa12"
},
"headers": {
"user-agent": [
"Go-http-client/1.1"
]
},
"id": "087dcbfb-3065-5fe5-227a-9d4853f558f3",
"mount_accessor": "auth_kubernetes_b944d443",
"mount_class": "auth",
"mount_point": "auth/kubernetes/",
"mount_running_version": "v0.21.0+builtin",
"mount_type": "kubernetes",
"namespace": {
"id": "root"
},
"operation": "update",
"path": "auth/kubernetes/login",
"remote_address": "10.244.0.35",
"remote_port": 58634
},
"time": "2025-04-16T08:43:21.057004259Z",
"type": "request"
}
- 1-응답 : 토큰 받음
{
"auth": {
"accessor": "hmac-sha256:d4f4e30a932a0580a83e4486a38f7682364c59af0103c8775f78de603ef175c5",
"client_token": "hmac-sha256:041a17e266dc6d22b5119df83745c22e91e38d14f36b0942ffbe9abe2298ebf3",
"display_name": "kubernetes-default-vault",
"entity_id": "481c4eec-74be-2fd5-2c78-ce8108a69fef",
"metadata": {
"role": "webapp",
"service_account_name": "vault",
"service_account_namespace": "default",
"service_account_secret_name": "",
"service_account_uid": "7683a434-3db0-42a2-8f29-316d74275d2b"
},
"policies": [
"default",
"webapp"
],
"token_policies": [
"default",
"webapp"
],
"token_ttl": 86400,
"token_type": "service"
},
"request": {
"data": {
"jwt": "hmac-sha256:ef7ef69ada78306a869b639647c1a4e647ab7cd448ed42137ef528a1c9244575",
"role": "hmac-sha256:00d7e66d7673210eeb1434c40d7a56b53d583857c1d1dd5fcb32a4abfb3ffa12"
},
"headers": {
"user-agent": [
"Go-http-client/1.1"
]
},
"id": "087dcbfb-3065-5fe5-227a-9d4853f558f3",
"mount_accessor": "auth_kubernetes_b944d443",
"mount_class": "auth",
"mount_point": "auth/kubernetes/",
"mount_running_version": "v0.21.0+builtin",
"mount_type": "kubernetes",
"namespace": {
"id": "root"
},
"operation": "update",
"path": "auth/kubernetes/login",
"remote_address": "10.244.0.35",
"remote_port": 58634
},
"response": {
"auth": {
"accessor": "hmac-sha256:d4f4e30a932a0580a83e4486a38f7682364c59af0103c8775f78de603ef175c5",
"client_token": "hmac-sha256:041a17e266dc6d22b5119df83745c22e91e38d14f36b0942ffbe9abe2298ebf3",
"display_name": "kubernetes-default-vault",
"entity_id": "481c4eec-74be-2fd5-2c78-ce8108a69fef",
"metadata": {
"role": "webapp",
"service_account_name": "vault",
"service_account_namespace": "default",
"service_account_secret_name": "",
"service_account_uid": "7683a434-3db0-42a2-8f29-316d74275d2b"
},
"policies": [
"default",
"webapp"
],
"token_policies": [
"default",
"webapp"
],
"token_ttl": 86400,
"token_type": "service"
},
"mount_accessor": "auth_kubernetes_b944d443",
"mount_class": "auth",
"mount_point": "auth/kubernetes/",
"mount_running_plugin_version": "v0.21.0+builtin",
"mount_type": "kubernetes"
},
"time": "2025-04-16T08:43:21.060132801Z",
"type": "response"
}
- 2-요청 : 발급 받은 토큰으로 secret 요청
{
"auth": {
"accessor": "hmac-sha256:d4f4e30a932a0580a83e4486a38f7682364c59af0103c8775f78de603ef175c5",
"client_token": "hmac-sha256:041a17e266dc6d22b5119df83745c22e91e38d14f36b0942ffbe9abe2298ebf3",
"display_name": "kubernetes-default-vault",
"entity_id": "481c4eec-74be-2fd5-2c78-ce8108a69fef",
"metadata": {
"role": "webapp",
"service_account_name": "vault",
"service_account_namespace": "default",
"service_account_secret_name": "",
"service_account_uid": "7683a434-3db0-42a2-8f29-316d74275d2b"
},
"policies": [
"default",
"webapp"
],
"policy_results": {
"allowed": true,
"granting_policies": [
{
"type": ""
},
{
"name": "webapp",
"namespace_id": "root",
"type": "acl"
}
]
},
"token_policies": [
"default",
"webapp"
],
"token_issue_time": "2025-04-16T08:43:21Z",
"token_ttl": 86400,
"token_type": "service"
},
"request": {
"client_id": "481c4eec-74be-2fd5-2c78-ce8108a69fef",
"client_token": "hmac-sha256:81e4d35d947f4dc6786cd349da166303875cdbd64b13318e71fe94071b454d32",
"client_token_accessor": "hmac-sha256:d4f4e30a932a0580a83e4486a38f7682364c59af0103c8775f78de603ef175c5",
"headers": {
"user-agent": [
"Go-http-client/1.1"
]
},
"id": "f58cfeeb-cbec-cc3c-4b14-7391d0b341ee",
"mount_class": "secret",
"mount_point": "secret/",
"mount_running_version": "v0.21.0+builtin",
"mount_type": "kv",
"namespace": {
"id": "root"
},
"operation": "read",
"path": "secret/data/webapp/config",
"remote_address": "10.244.0.35",
"remote_port": 58634
},
"time": "2025-04-16T08:43:21.061388593Z",
"type": "request"
}
- 2-응답 : secret 받음
{
"auth": {
"accessor": "hmac-sha256:d4f4e30a932a0580a83e4486a38f7682364c59af0103c8775f78de603ef175c5",
"client_token": "hmac-sha256:041a17e266dc6d22b5119df83745c22e91e38d14f36b0942ffbe9abe2298ebf3",
"display_name": "kubernetes-default-vault",
"entity_id": "481c4eec-74be-2fd5-2c78-ce8108a69fef",
"metadata": {
"role": "webapp",
"service_account_name": "vault",
"service_account_namespace": "default",
"service_account_secret_name": "",
"service_account_uid": "7683a434-3db0-42a2-8f29-316d74275d2b"
},
"policies": [
"default",
"webapp"
],
"policy_results": {
"allowed": true,
"granting_policies": [
{
"type": ""
},
{
"name": "webapp",
"namespace_id": "root",
"type": "acl"
}
]
},
"token_policies": [
"default",
"webapp"
],
"token_issue_time": "2025-04-16T08:43:21Z",
"token_ttl": 86400,
"token_type": "service"
},
"request": {
"client_id": "481c4eec-74be-2fd5-2c78-ce8108a69fef",
"client_token": "hmac-sha256:81e4d35d947f4dc6786cd349da166303875cdbd64b13318e71fe94071b454d32",
"client_token_accessor": "hmac-sha256:d4f4e30a932a0580a83e4486a38f7682364c59af0103c8775f78de603ef175c5",
"headers": {
"user-agent": [
"Go-http-client/1.1"
]
},
"id": "f58cfeeb-cbec-cc3c-4b14-7391d0b341ee",
"mount_accessor": "kv_c002faad",
"mount_class": "secret",
"mount_point": "secret/",
"mount_running_version": "v0.21.0+builtin",
"mount_type": "kv",
"namespace": {
"id": "root"
},
"operation": "read",
"path": "secret/data/webapp/config",
"remote_address": "10.244.0.35",
"remote_port": 58634
},
"response": {
"data": {
"data": {
"password": "hmac-sha256:fdd33a2c65e563d1d0c2e026f3e71d7f457b66ed50c2efdb67d9d761a63a6a05",
"username": "hmac-sha256:b74d0b553a23887a509ac85f5b71e91b184badcd1b76072d85d6cd7ce549b5db"
},
"metadata": {
"created_time": "hmac-sha256:b17c0c831c5ce2d76ce9c177f0f95670aed260d8f21e4ee58715dd5acbb7df4a",
"custom_metadata": null,
"deletion_time": "hmac-sha256:a528e8995baee2920f8276eecbdfdb7f0dfa689c0ac51c604ff4715de5c60219",
"destroyed": false,
"version": 1
}
},
"mount_accessor": "kv_c002faad",
"mount_class": "secret",
"mount_point": "secret/",
"mount_running_plugin_version": "v0.21.0+builtin",
"mount_type": "kv"
},
"time": "2025-04-16T08:43:21.061646884Z",
"type": "response"
}

- 실습 후 실습 환경 삭제 kind delete cluster --name myk8s
- (참고) Pod certificates : K8S 1.34(알파), 1.35(베타)는 파드 인증서 직접 제공 - Docs , Issue
Vault Secrets Operaor (VSO)
1. VSO란
- VSO는 k8S Native Secret 를 업데이트 및 관리, 개발자가 Vault 도구 학습할 필요가 없음 - Docs

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

2. K8S (kind) 설치
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 ps
kubectl get node
# 노드에 기본 툴 설치
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'
3. Vault 설치 : dev 모드 활성화 설치 - Docs , Github , Chart-Vault
# 공식 문서 버전 정보
helm search repo hashicorp/vault
NAME CHART VERSION APP VERSION DESCRIPTION
hashicorp/vault 0.28.1 1.17.2 Official HashiCorp Vault Chart
hashicorp/vault-secrets-operator 0.7.1 0.8.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) 설정 파일 작성 : 직접 Unseal 하지 않아도됨. RootToken 직접 설정
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
# 확인
kubectl get pods -n vault
NAME READY STATUS RESTARTS AGE
vault-0 1/1 Running 0 14m
4. Vault 설정
# Vault 로그인 : 토큰(root)
export VAULT_ADDR='http://localhost:30000'
vault login
Token (will be hidden): root
...
# kubernetes 인증 활성화
vault auth enable -path demo-auth-mount kubernetes
Success! Enabled kubernetes auth method at: demo-auth-mount/
vault write auth/demo-auth-mount/config kubernetes_host="https://kubernetes.default.svc"
# 시크릿(엔진v2) 활성화
vault secrets enable -path=kvv2 kv-v2
Success! Enabled the kv-v2 secrets engine at: kvv2/
# Create a JSON file with a Vault policy.
tee webapp.json <<EOF
path "kvv2/data/webapp/config" {
capabilities = ["read", "list"]
}
EOF
vault policy write webapp webapp.json
# Create a role in Vault to enable access to secrets within the kv v2 secrets engine.
## Notice that the bound_service_account_namespaces is app, limiting which namespace the secret is synced to.
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
# Create a 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-04-16T12:09:42.364538501Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
5. Vault Secrets Operator (VSO) 소개
- https://docmoa.github.io/04-HashiCorp/06-Vault/01-Information/vault-secret-operator/1-vso-overview.html
- Kubernetes에서 Vault의 비밀(Secrets, Dynamic Credentials)을 안전하게 가져와서 Kubernetes Secret 리소스에 자동 반영하는 Operator.
flowchart LR
subgraph K8s["Kubernetes Cluster"]
A[VaultAuth CRD]
B[VaultStaticSecret / VaultDynamicSecret / VaultPKISecret CRD]
C[Vault Secrets Operator - Controller Manager]
D[Kubernetes API Server]
E[Kubernetes Secret - Synced Secret]
F[Application Pod]
end
subgraph Vault["HashiCorp Vault"]
VA[Auth Methods : Kubernetes / AppRole / JWT]
VP[Secret Engines : KV, Transit, PKI, Database, AWS, etc.]
end
A -- Watch --> C
B -- Watch --> C
C -- Authenticate --> VA
C -- Read/Generate Secrets --> VP
C -- Sync to K8s Secret --> D
D -- Store --> E
E -- Mount/Env Inject --> F
- Kubernetes Controller 형태로 동작
- 사용자 정의 CRD(Custom Resource)를 감시
- VaultAuth
- VaultStaticSecret
- VaultDynamicSecret
- VaultPKISecret
- 변경 감지 시 Vault API를 호출하여 새로운 Credentials/Secret 을 가져온 뒤 K8s Secret 생성/갱신 수행
- VaultAuth (CRD)
- Kubernetes에서 Vault 인증 방식(Kubernetes Auth, AppRole Auth 등)을 정의하는 리소스
- auth method: kubernetes, approle, jwt
- mount: auth path
- role: Vault role
- namespace: Vault namespace
- VSO는 VaultAuth 객체를 통해 Vault에 로그인하고 토큰을 캐싱하여 사용.
- Kubernetes에서 Vault 인증 방식(Kubernetes Auth, AppRole Auth 등)을 정의하는 리소스
- Secret Syncing Controller
- VaultSecret CRD를 감시하여 다음을 수행
- Static Secrets → 지정 경로에서 secret을 주기적으로 읽어서 K8s Secret에 반영
- Dynamic Secrets → (예: DB Creds, AWS Creds) 자동 rotate → Secret 업데이트
- PKI Certificates → cert 자동 생성/renewal
- Kubernetes API Server
- Vault Secrets Operator가 생성/갱신하는 Kubernetes Secret을 저장하는 역할.
sequenceDiagram
participant User as User
participant CRD as VaultAuth/VaultSecret CRD
participant VSO as Vault Secrets Operator
participant Vault as Vault API
participant API as Kubernetes API Server
participant Pod as Application Pod
User ->> CRD: Apply VaultAuth / VaultSecret
VSO ->> CRD: Watch for changes
VSO ->> Vault: Authenticate using VaultAuth
Vault -->> VSO: Return Vault token
VSO ->> Vault: Read/Generate Secret (KV/DB/PKI)
Vault -->> VSO: Secret data
VSO ->> API: Create/Update Kubernetes Secret
API -->> Pod: Make Secret available (mount/env)
Pod ->> Pod: Consume Secret
- 전체 동작 흐름 요약
- 사용자가 VaultAuth, VaultStaticSecret 등의 CRD 생성
- VSO가 CRD를 감시하여 필요 시 Vault에 인증
- Vault에서 secret/dynamic credentials/pki cert 가져옴
- Kubernetes Secret 객체로 생성/업데이트
- 애플리케이션 Pod는 일반 Secret처럼 mount/inject 하여 사용
6. Vault Secrets Operator (VSO) 설치 - HelmChart
# 공식 문서 버전 정보
helm search repo hashicorp/vault
NAME CHART VERSION APP VERSION DESCRIPTION
hashicorp/vault 0.28.1 1.17.2 Official HashiCorp Vault Chart
hashicorp/vault-secrets-operator 0.7.1 0.8.0 Official Vault Secrets Operator Chart
# 파일 확인
cat vault/vault-operator-values.yaml
...
# VSO 설치 : Helm v4 실패
helm version
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
Error: INSTALLATION FAILED: VaultAuth.secrets.hashicorp.com "vault-secrets-operator-default-transit-auth" is invalid: spec.namespace: Invalid value: "null": spec.namespace in body must be of type string: "null"
helm list -A
helm uninstall -n vault-secrets-operator-system vault-secrets-operator
# Helm v3 으로 설치
docker exec -it myk8s-control-plane bash
------------------------------------
# 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
#
cat << EOF > vault-operator-values.yaml
defaultVaultConnection:
enabled: true
address: "http://vault.vault.svc.cluster.local:8200"
skipTLSVerify: false
controller:
manager:
clientCache:
persistenceModel: direct-encrypted
storageEncryption:
enabled: true
mount: k8s-auth-mount
keyName: vso-client-cache
transitMount: demo-transit
kubernetes:
role: auth-role-operator
serviceAccount: vault-secrets-operator-controller-manager
tokenAudiences: ["vault"]
EOF
helm install vault-secrets-operator hashicorp/vault-secrets-operator -n vault-secrets-operator-system --create-namespace --values vault-operator-values.yaml --version 0.10.0
helm list -A
exit
------------------------------------
# 설치 확인
kubectl get-all -n vault-secrets-operator-system
kubectl get crd | grep secrets.hashicorp.com
...
secrettransformations.secrets.hashicorp.com 2025-04-16T12:10:53Z
vaultauthglobals.secrets.hashicorp.com 2025-04-16T12:10:53Z
vaultauths.secrets.hashicorp.com 2025-04-16T12:10:53Z
vaultconnections.secrets.hashicorp.com 2025-04-16T12:10:53Z
vaultdynamicsecrets.secrets.hashicorp.com 2025-04-16T12:10:53Z
vaultpkisecrets.secrets.hashicorp.com 2025-04-16T12:10:53Z
vaultstaticsecrets.secrets.hashicorp.com 2025-04-16T12:10:53Z
# vso 파드 상세 정보 확인 : 2개의 컨테이너로 구성
kubectl describe pod -n vault-secrets-operator-system
...
Service Account: vault-secrets-operator-controller-manager
...
Containers:
kube-rbac-proxy:
Container ID: containerd://db3eae7b836fb4f1b4236c494c8fa96ada94769a6c602e1a150c75293a6a4162
Image: quay.io/brancz/kube-rbac-proxy:v0.18.1
...
manager:
Container ID: containerd://1ab1545fb4bd86ac52d6c7609a3e962cd2d1a81daa9bbd9c82f79d9a0d8b6466
Image: hashicorp/vault-secrets-operator:0.10.0
...
# CRD 확인
kubectl get vaultconnections,vaultauths -n vault-secrets-operator-system
NAME AGE
vaultconnection.secrets.hashicorp.com/default 3m21s
NAME AGE
vaultauth.secrets.hashicorp.com/vault-secrets-operator-default-transit-auth 3m21s
# 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": "demo-auth-mount",
"storageEncryption": {
"keyName": "vso-client-cache",
"mount": "demo-transit"
},
"vaultConnectionRef": "default"
}
# vaultconnection CRD 확인
kubectl get vaultconnection -n vault-secrets-operator-system default -o jsonpath='{.spec}' | jq
{
"address": "http://vault.vault.svc.cluster.local:8200",
"skipTLSVerify": false
}
# 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
# VSO는 deployment 등에 Secret 적용을 위한 rollout(G W P U) 필요, 특히 vault 서버로 부터 암호 값을 가져와서 secret 에 업데이트 및 관리 필요함.
## kubectl rollout restart(GET, PATCH), rollout status(GET, WATCH), rollout undo(GET, UPDATE)
## G (Get), L(List), W(Watch), P(Patch), C(Create), U(Update), D(Delete), DC(DeleteCollection)
kubectl rolesum -n vault-secrets-operator-system vault-secrets-operator-controller-manager

7. static secret 고정 암호 실습 시나리오

- Vault 에 Secret/Policy/Role 생성
- VSO 가 → Vault 에 로그인 후 Token 받음 ← VSO 에 대한 확인 역시 K8S API 를 통해 확인
- VSO가 Vault 에 로그인 과정은 VaultAuth CRD를 통해서 작동
- VSO 가 전달받은 Token 으로 담아 → Vault 에 Secret 요청 후 받음
- VSO가 Vault 에 Secret 요청 과정은 VaultStaticSecret CRD를 통해서 작동
- VSO 는 K8S Secret 에 값을 업데이트
- VSO 는 주기적으로(현재 설정은 30초 마다) → Vault 에 Secret 요청 후 받음
- Deploy and sync a secret
#
kubectl create ns app
# CRD 확인 : vaultauths
kubectl explain vaultauths
kubectl explain vaultauths.spec
# Set up Kubernetes authentication for the secret.
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
#
kubectl get sa,vaultauth -n app
NAME SECRETS AGE
serviceaccount/default 0 23m
serviceaccount/demo-static-app 0 21m
NAME AGE
vaultauth.secrets.hashicorp.com/static-auth 21m
# CRD 확인 : vaultstaticsecrets
kubectl explain vaultstaticsecrets
kubectl explain vaultstaticsecrets.spec
# Create the secret names secretkv in the app namespace.
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
#
kubectl get vaultstaticsecret -n app
NAME AGE
vault-kv-app 11s
8. (심화) VSO → Vault 서버에 요청 호출 트래픽 분석 ⇒ VSO는 K8S Secret 에 업데이트 수행.
- 1번 요청 : GET /v1/sys/seal-status → 응답 : seal 상태 정보 받음
# 요청
T 2025/04/16 13:37:06.797268 10.244.0.7:56744 -> 10.96.14.99:8200 [AP] #4
GET /v1/sys/seal-status HTTP/1.1.
Host: vault.vault.svc.cluster.local:8200.
User-Agent: Go-http-client/1.1.
X-Vault-Request: true.
Accept-Encoding: gzip.
.
# 응답
T 2025/04/16 13:37:06.798044 10.96.14.99:8200 -> 10.244.0.7:56744 [AP] #6
HTTP/1.1 200 OK.
Cache-Control: no-store.
Content-Type: application/json.
Strict-Transport-Security: max-age=31536000; includeSubDomains.
Date: Wed, 16 Apr 2025 13:37:06 GMT.
Content-Length: 298.
.
{
"type": "shamir",
"initialized": true,
"sealed": false,
"t": 1,
"n": 1,
"progress": 0,
"nonce": "",
"version": "1.19.0",
"build_date": "2025-03-04T12:36:40Z",
"migration": false,
"cluster_name": "vault-cluster-bee86515",
"cluster_id": "9e824588-2328-b935-bc56-b60dd55da098",
"recovery_seal": false,
"storage_type": "inmem"
}
- 2번 요청 : PUT /v1/auth/demo-auth-mount/login → 응답 : 로그인 되어 Token 받음
# 요청
T 2025/04/16 13:37:06.913301 10.244.0.7:56756 -> 10.96.14.99:8200 [AP] #18
PUT /v1/auth/demo-auth-mount/login HTTP/1.1.
Host: vault.vault.svc.cluster.local:8200.
User-Agent: Go-http-client/1.1.
Content-Length: 914.
X-Vault-Request: true.
Accept-Encoding: gzip.
.
{
"jwt": "eyJhbGciOiJSUzI1NiIsImtpZCI6InlXNU9kWHRrbEsyd3VGenhfUm0tZjlydTROOUFpcXpWcTkxWEN3N1k1YXcifQ.eyJhdWQiOlsidmF1bHQiXSwiZXhwIjoxNzQ0ODExMjI2LCJpYXQiOjE3NDQ4MTA2MjYsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNzgzMDgzZmEtMWM4Ni00MjQ5LTgxYzYtOTNhMTliYjBmOWIzIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJhcHAiLCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoiZGVtby1zdGF0aWMtYXBwIiwidWlkIjoiOWQ1ZjljMjYtMTg2My00ZTIzLTk1MDAtNjhiMzgzNDk1NTljIn19LCJuYmYiOjE3NDQ4MTA2MjYsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDphcHA6ZGVtby1zdGF0aWMtYXBwIn0.VB28mht-udAvxKE9eOn_LYPFgRWZ8pVNwukpeKAU0U5VDyBa9A2OK61BDai1WrqVmjr6PcZhofjZSraXqIy-MZh3QIcZ0sK0b2H0EjvL9wv6Xo7D0rhyaQEK3prIbLcsXwnbcV480HArmGaWq3OYAjZqKRtuGnarFm5r5GsOL7k01G8IlyEmUeWsz-lb-YQY_2KJ2dUzny5IYKptBKYmX-N8ekRqCcAVhsvZxHXXBCIyJRt4HaAV3DfTGEybcgSZBCMqwxtQ3RrbMc0E4ahuAvzjK1lQ0b1-J14e5oumvbAehLJZRjf9mC6xOyCFm2EJDMAifO3q-8LQptuqBFRGVA",
"role": "role1"
}
# 응답
T 2025/04/16 13:37:06.920956 10.96.14.99:8200 -> 10.244.0.7:56756 [AP] #20
HTTP/1.1 200 OK.
Cache-Control: no-store.
Content-Type: application/json.
Strict-Transport-Security: max-age=31536000; includeSubDomains.
Date: Wed, 16 Apr 2025 13:37:06 GMT.
Content-Length: 757.
.
{
"request_id": "cd3be14f-717b-119b-fcff-907833117136",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": null,
"wrap_info": null,
"warnings": null,
"auth": {
"client_token": "hvs.CAESIJhygVV1IBMyGElsYH232_kCjjGn9d88FOq1aA9R9UZEGh4KHGh2cy5GM1EyRGZRMHNoOVplUTZtRkVMSlpaQUs",
"accessor": "oqnTyuom26z6ITMBU4g5f2ON",
"policies": [
"default",
"webapp"
],
"token_policies": [
"default",
"webapp"
],
"metadata": {
"role": "role1",
"service_account_name": "demo-static-app",
"service_account_namespace": "app",
"service_account_secret_name": "",
"service_account_uid": "9d5f9c26-1863-4e23-9500-68b38349559c"
},
"lease_duration": 86400,
"renewable": true,
"entity_id": "92ec4e7f-d8d4-0064-f0c3-ccc8dce70671",
"token_type": "service",
"orphan": true,
"mfa_requirement": null,
"num_uses": 0
},
"mount_type": ""
}
- 3번 요청 : GET /v1/kvv2/data/webapp/config → 응답 : 해당 시크릿 값 받음
# 요청
T 2025/04/16 13:37:06.935065 10.244.0.7:56756 -> 10.96.14.99:8200 [AP] #33
GET /v1/kvv2/data/webapp/config HTTP/1.1.
Host: vault.vault.svc.cluster.local:8200.
User-Agent: Go-http-client/1.1.
X-Vault-Request: true.
X-Vault-Token: hvs.CAESIJhygVV1IBMyGElsYH232_kCjjGn9d88FOq1aA9R9UZEGh4KHGh2cy5GM1EyRGZRMHNoOVplUTZtRkVMSlpaQUs.
Accept-Encoding: gzip.
.
# 응답
T 2025/04/16 13:37:06.935818 10.96.14.99:8200 -> 10.244.0.7:56756 [AP] #34
HTTP/1.1 200 OK.
Cache-Control: no-store.
Content-Type: application/json.
Strict-Transport-Security: max-age=31536000; includeSubDomains.
Date: Wed, 16 Apr 2025 13:37:06 GMT.
Content-Length: 375.
.
{
"request_id": "e273bd93-29b0-fce8-42f7-bae54e1a71b8",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": {
"data": {
"password": "static-password3",
"username": "static-user3"
},
"metadata": {
"created_time": "2025-04-16T13:15:35.928947804Z",
"custom_metadata": null,
"deletion_time": "",
"destroyed": false,
"version": 3
}
},
"wrap_info": null,
"warnings": null,
"auth": null,
"mount_type": "kv"
}
- VSO 는 설정된 주기(현재 30초) 마다 Vault 서버에 GET 요청으로 시크릿 값을 받음
- Rotate the static secret
# K8S Secret 확인
kubectl get secret -n app
NAME TYPE DATA AGE
secretkv Opaque 3 3m10s
# K8S 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-04-16T12:09:42.364538501Z","custom_metadata":null,"deletion_time":"","destroyed":false,"version":1}}'
password='static-password'
username='static-user'
# 시크릿 업데이트 Rotate the secret.
vault kv put kvv2/webapp/config username="static-user2" password="static-password2"
===== Secret Path =====
kvv2/data/webapp/config
======= Metadata =======
Key Value
--- -----
created_time 2025-04-16T13:03:15.530981752Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 2
# K8S Secret 값 확인 >> 이후 VSO 는 설정된 주기(현재 30초) 마다 Vault 서버에 GET 요청으로 시크릿 값을 받음
kubectl view-secret -n app secretkv --all
_raw='{"data":{"password":"static-password2","username":"static-user2"},"metadata":{"created_time":"2025-04-16T13:03:15.530981752Z","custom_metadata":null,"deletion_time":"","destroyed":false,"version":2}}'
password='static-password2'
username='static-user2'
# secretkv 리소스의 AGE를 보면 재성성되지 않았고, 리소스 data 의 값만 바꿈
kubectl get secret -n app
NAME TYPE DATA AGE
secretkv Opaque 3 8m


Dynamic Secret 동적 암호 주기 관리 실습 시나리오

- 동적 암호 주기 관리는 Vault 가 자동으로 암호를 갱신(삭제/재생성)하고, VSO가 해당 암호를 K8S Secret 에 동기화
- Dynamic secrets lifecycle is managed by Vault and will be automatically rotated
- The lifecycle management includes deleting and recreating the secret
- 주요 사용 CRD
- VSO가 Vault 에 로그인 과정은 VaultAuth CRD를 통해서 작동
- VSO가 Vault 에 Dynamic Secret 요청 과정은 VaultDynamicSecret CRD를 통해서 작동
1. PostgreSQL 파드 배포 및 Vault Database Secret Engine 설정 - Docs , ClientCache
#
kubectl create ns postgres
# Add the Bitnami repository to your local Helm.
helm repo add bitnami https://charts.bitnami.com/bitnami
# Install PostgreSQL : 암호 secret-pass
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
postgres_password='secret-pass'
# psql 로그인 확인
kubectl exec -it -n postgres postgres-postgresql-0 -- sh -c 'PGPASSWORD=secret-pass psql -U postgres -h localhost'
kubectl exec -it -n postgres postgres-postgresql-0 -- sh -c "PGPASSWORD=secret-pass psql -U postgres -h localhost -c '\l'"
q 빠져나오기

# Enable an instance of the Database Secrets Engine
vault secrets enable -path=demo-db database
Success! Enabled the database secrets engine at: demo-db/
# Configure the Database Secrets Engine : 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"
Success! Data written to: demo-db/config/demo-db
# 확인 : user,pw는 조금 더 안전하게 변수 처리
vault read demo-db/config/demo-db
Key Value
--- -----
allowed_roles [dev-postgres]
connection_details map[connection_url:postgresql://{{username}}:{{password}}@postgres-postgresql.postgres.svc.cluster.local:5432/postgres?sslmode=disable username:postgres]
disable_automated_rotation false
password_policy n/a
plugin_name postgresql-database-plugin
plugin_version n/a
root_credentials_rotate_statements []
rotation_period 0s
rotation_schedule n/a
rotation_window 0
skip_static_role_import_rotation false
verify_connection true
# DB 사용자 동적 생성 Role 등록
# Create a role for the PostgreSQL pod : default_ttl="10m"(인증 생성 후 10분 유효), max_ttl="10m"(연장 요청 해도 20분 못넘음)
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"
# creation_statements="..." : Vault가 동적으로 사용자 생성 시 실행할 SQL 문을 정의
## {{name}}, {{password}}, {{expiration}}은 Vault가 자동으로 치환하는 템플릿 변수
## 새로운 PostgreSQL 사용자 생성 (CREATE ROLE) , 비밀번호와 만료시간 설정 , 해당 사용자에게 postgres DB에 대한 모든 권한 부여
# revocation_statements="..." : Vault가 사용자 자격을 취소(revoke)할 때 실행할 SQL
## 해당 사용자로부터 postgres DB의 모든 권한을 제거합니다.
## 사용자를 아예 DROP하지 않는 경우도 많음 → 보안 정책에 따라 추가 가능.
# 확인
vault read demo-db/roles/dev-postgres
Key Value
--- -----
creation_statements [CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT ALL PRIVILEGES ON DATABASE postgres TO "{{name}}";]
credential_type password
db_name demo-db
default_ttl 10m
max_ttl 20m
renew_statements []
revocation_statements [REVOKE ALL ON DATABASE postgres FROM "{{name}}";]
rollback_statements []
# Create the demo-auth-policy-db policy.
vault policy write demo-auth-policy-db - <<EOF
path "demo-db/creds/dev-postgres" {
capabilities = ["read"]
}
EOF
Success! Uploaded policy: demo-auth-policy-db
# psql 확인
kubectl exec -it -n postgres postgres-postgresql-0 -- sh -c "PGPASSWORD=secret-pass psql -U postgres -h localhost -c '\du'"

2. Vault의 PostgreSQL용 동적 시크릿 엔진을 사용하여 임시 클라이언트 자격 증명을 생성
# Create a new role for the dynamic secret.
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
# 확인
vault read auth/demo-auth-mount/role/auth-role

PKI secret
PKI 인증서를 사용하는 애플리케이션에서 동적으로 변경된 PKI 인증서를 사용할 수 있도록 지원
1. 기본 설정 - Docs
# Enable the PKI secrets engine at its default path.
vault secrets enable pki
Success! Enabled the pki secrets engine at: pki/
# By default the KPI secrets engine sets the time-to-live (TTL) to 30 days
# Configure the max lease time-to-live (TTL) to 8760h.
vault secrets tune -max-lease-ttl=8760h pki
# Vault can accept an existing key pair, or it can generate its own self-signed root.
# In general, we recommend maintaining your root CA outside of Vault and providing Vault a signed intermediate CA.
# Generate a self-signed certificate valid for 8760h
vault write pki/root/generate/internal \
common_name=example.com \
ttl=8760h
...
Key Value
--- -----
certificate -----BEGIN CERTIFICATE-----
## ...
-----END CERTIFICATE-----
expiration 1619120269
issuing_ca -----BEGIN CERTIFICATE-----
## ...
-----END CERTIFICATE-----
serial_number 65:37:b5:b3:91:6c:7b:d8:33:22:03:28:b1:58:ff:be:8a:72:a4:c0
#
echo "-----BEGIN CERTIFICATE-----..." | openssl x509 -noout -text
...
Serial Number:
74:5e:64:1b:e6:f9:74:2b:16:19:35:b6:c7:8d:99:2f:aa:89:d9:16
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=example.com
Validity
Not Before: Nov 30 04:20:41 2025 GMT
Not After : Nov 30 04:21:11 2026 GMT
Subject: CN=example.com
...
X509v3 extensions:
X509v3 Key Usage: critical
Certificate Sign, CRL Sign
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Subject Key Identifier:
28:8D:81:68:47:27:3C:93:6A:C1:DE:40:BE:41:22:44:85:87:A9:E7
X509v3 Authority Key Identifier:
28:8D:81:68:47:27:3C:93:6A:C1:DE:40:BE:41:22:44:85:87:A9:E7
X509v3 Subject Alternative Name:
DNS:example.com
# Vault의 PKI secrets engine(pki 마운트)에 대해 인증서 발급자(CA) 접근 URL과 CRL(인증서 폐기 목록) 접근 URL을 설정.
# 이 설정 값들은 Vault가 새로 발급하는 X.509 인증서의 확장 필드(AIA / CDP)에 포함되어 클라이언트와 인증서 검증자에게 전달됨.
# Configure the PKI secrets engine certificate issuing and certificate revocation list (CRL) endpoints to use the Vault service in the default namespace.
## issuing_certificates="http://vault.vault.svc:8200/v1/pki/ca" # 발급자(issuer) 인증서(또는 CA 번들)를 가져오는 URL
## crl_distribution_points="http://vault.vault.svc:8200/v1/pki/crl" # CRL(Certificate Revocation List)을 내려받을 수 있는 URL
vault write pki/config/urls \
issuing_certificates="http://vault.vault.svc:8200/v1/pki/ca" \
crl_distribution_points="http://vault.vault.svc:8200/v1/pki/crl"
# Vault가 이제 발급하는 모든 인증서에 다음과 같은 확장(extension)을 포함할 수 있습니다:
## Authority Information Access (AIA) 또는 Issuing Certificate 관련 extension → issuing_certificates URL 포함
## CRL Distribution Points (CDP) → crl_distribution_points URL 포함
# 클라이언트(또는 CA 체인 검증 라이브러리)는 인증서의 AIA/CDP를 보고 해당 URL로 접속하여 CA 인증서나 CRL을 내려받아 검증에 사용합니다.
# 즉, 이 설정으로 인해 인증서의 메타정보가 외부(또는 내부) URL을 통해 자동으로 유효성/폐기 여부를 확인할 수 있게 됩니다.
vault read pki/config/urls
# 아래 ca 파일 다운로드 후 확인 해보자
open "http://127.0.0.1:30000/v1/pki/ca" # openssl x509 -noout -text -in ~/Downloads/ca
curl -s http://127.0.0.1:30000/v1/pki/ca | openssl x509 -text -noout -inform DER
open "http://127.0.0.1:30000/v1/pki/crl" # openssl crl -noout -text -in ~/Downloads/crl
# Configure a role named example-dot-com that enables the creation of certificates example.com domain with any subdomains.
## The role, example-dot-com, is a logical name that maps to a policy used to generate credentials.
## This generates a number of endpoints that are used by the Kubernetes service account to issue and sign these certificates.
## A policy must be created that enables these paths.
vault write pki/roles/example-dot-com \
allowed_domains=example.com \
allow_subdomains=true \
max_ttl=72h
# Create a policy named pki that enables read access to the PKI secrets engine paths.
## These paths enable the token to view all the roles created for this PKI secrets engine and
## access the sign and issues operations for the example-dot-com role.
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


2. 동작 설정 및 확인 : Cert Manager 활용
# kubernetes 인증 활성화
vault auth enable kubernetes
vault write auth/kubernetes/config kubernetes_host="https://kubernetes.default.svc"
# Create a Kubernetes authentication role named issuer that binds the pki policy with a Kubernetes service account named issuer.
vault write auth/kubernetes/role/issuer \
bound_service_account_names=issuer \
bound_service_account_namespaces=default \
policies=pki \
ttl=20m
# Deploy Cert Manager
kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v1.12.3/cert-manager.crds.yaml
kubectl get crd | grep cert-manager
kubectl create namespace cert-manager
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager --namespace cert-manager --version v1.12.3 jetstack/cert-manager
kubectl get pods --namespace cert-manager
# Configure an issuer and generate a certificate
# Create a service account named issuer within the default namespace.
kubectl create serviceaccount issuer
# The service account generated a secret that is required by the Issuer automatically in Kubernetes 1.23.
# In Kubernetes 1.24+, you need to create the secret explicitly.
# Create an issuer secret.
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-token
EOF
kubectl apply -f issuer-secret.yaml
kubectl get secrets # The issuer secret is displayed here as the secret prefixed with issuer-token
NAME TYPE DATA AGE
issuer-token-lmzpj kubernetes.io/service-account-token 3 28s
# Create a variable named ISSUER_SECRET_REF to capture the secret name.
ISSUER_SECRET_REF=$(kubectl get secrets --output=json | jq -r '.items[].metadata | select(.name|startswith("issuer-token-")).name')
echo $ISSUER_SECRET_REF
kubectl view-secret $ISSUER_SECRET_REF --all
# Define an Issuer, named vault-issuer, that sets Vault as a certificate issuer.
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
# Create the vault-issuer Issuer.
# The specification defines the signing endpoint and the authentication endpoint and credentials.
## metadata.name sets the name of the Issuer to vault-issuer
## spec.vault.server sets the server address to the Kubernetes service created in the default namespace
## spec.vault.path is the signing endpoint created by Vault PKI example-dot-com role
## spec.vault.auth.kubernetes.mountPath sets the Vault authentication endpoint
## spec.vault.auth.kubernetes.role sets the Vault Kubernetes role to issuer
## spec.vault.auth.kubernetes/secretRef.name sets the secret for the Kubernetes service account
## spec.vault.auth.kubernetes/secretRef.key sets the type to token.
kubectl apply --filename vault-issuer.yaml
kubectl get issuer.cert-manager.io/vault-issuer
# Define a certificate named example-com.
# The Certificate, named example-com, requests from Vault the certificate through the Issuer, named vault-issuer.
# The common name and DNS names are names within the allowed domains for the configured Vault endpoint.
cat > example-com-cert.yaml <<EOF
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
kubectl apply --filename example-com-cert.yaml # Create the example-com certificate.
kubectl get certificate.cert-manager.io/example-com -owide
NAME READY SECRET ISSUER STATUS AGE
example-com False example-com-tls vault-issuer Issuing certificate as Secret does not exist 38s
# View the details of the example-com certificate.
# cert-manager가 Certificate 리소스를 처리하며 인증서를 발급하는 전체 흐름을 단계별로 확인.
## Certificate 리소스를 생성하면 cert-manager는:
1. Secret 에 기존 private key 있는지 확인
2. 없으면 새 private key 생성
3. CertificateRequest(CSR) 생성
4. Issuer/ClusterIssuer 에게 서명 요청
5. CA 또는 ACME 등에서 서명 받아 certificate 생성
6. 최종 Secret에 private key + cert chain 저장
7. Certificate 상태를 Issued 로 변경
kubectl describe certificate.cert-manager example-com
...
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
# cert-manager가 Certificate 를 살펴보니 Secret(example-com) 안에 private key(비밀키)가 존재하지 않기 때문에 새로 인증서를 발급해야 한다는 뜻.
# cert-manager가 하는 일 : Secret 조회, key 없음 → “새 key 필요 → 새 certificate 필요” 판단
Normal Issuing 18s cert-manager-certificates-trigger Issuing certificate as Secret does not contain a private key
# cert-manager가 새로운 private key를 생성하고, 임시 Secret에 저장했다는 뜻.
# cert-manager는 다음 CSR(CertificateRequest)을 만들기 위해 key를 잠시 임시 Secret에 저장한다:
# 안전하게 key를 먼저 준비 → 이후 최종 Secret에 병합하는 방식
Normal Generated 17s cert-manager-certificates-key-manager Stored new private key in temporary Secret resource "example-com-54lj7"
# cert-manager가 CertificateRequest(CSR) 를 생성했다는 의미.
# CSR은: 방금 만든 private key 기반으로 Issuer/ClusterIssuer 에게 "서명해 주세요" 요청하는 리소스
# 이 단계 후 보통 다음이 내부적으로 수행됨, issuer가 CSR을 확인하고 서명 처리 (예: Vault, ACME, CA, SelfSigned 등)
Normal Requested 17s cert-manager-certificates-request-manager Created new CertificateRequest resource "example-com-lmvtn"
# Issuer(예: Vault issuer, CA issuer, ACME issuer)가 CSR에 서명했고, cert-manager가 정상적으로 인증서를 발급받아 Secret에 저장했다는 의미.
# cert-manager는 수행 : CA response(인증서 체인) 수신 -> 최종 Secret(example-com)에 아래 내용 저장 -> CertificateReady = True
## private key (임시 Secret → 이동) , certificate (CRT), ca.crt (chain)
Normal Issuing 17s 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-lmvtn True True vault-issuer system:serviceaccount:cert-manager:cert-manager Certificate fetched from issuer successfully 7m52s
# 결과적으로 Secret/example-com 은 다음을 포함: tls.key , tls.crt . ca.crt (if issuer provides)
kubectl view-secret $ISSUER_SECRET_REF --all
ECRET_REF --all
ca.crt='-----BEGIN CERTIFICATE-----
MIID...
-----END CERTIFICATE-----
'
namespace='default'
tls.crt='-----BEGIN CERTIFICATE-----
MIIDyzCCArOgAwIBAgIUOSsThQd7MWGidC0qy8OaSBVKlA8wDQYJKoZIhvcNAQEL
BQAwFjEUMBIGA1UEAxMLZXhhbXBsZS5jb20wHhcNMjUxMTMwMDUwNjU1WhcNMjUx
MjAzMDUwNzI1WjAaMRgwFgYDVQQDEw93d3cuZXhhbXBsZS5jb20wggEiMA0GCSqG
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDgdctKHOD7LqlDMH8sNGmC/z6+74q7NuoS
9szMwB4ifaGb7NT40I0bG2Cffprzv+t58bfXf/dcAbbw4glq34x8sZEuMCWNtl/y
1llQMlAj9ld2tp/seP/85BvnE7cpk0tBWgXOQUCJ0qnF9AVS+FuwW9gHerK4k8N3
MZ+rKdrCaAVxOzK6DasrsCDmlppplCCVeW0VXka76ZPw53ubTwRAGjDfU6oYTV+K
tjlLAT9vXaXF58ZMermIZpBuoW70VnXwXTbXaI1+axSEsoIB7d9dsvS9lLRslRiw
V0+mBa6U9xzPdpmiukgnxFODNKlYpD6r7LFYMUHmtw67+5O4Qmz3AgMBAAGjggEL
MIIBBzAOBgNVHQ8BAf8EBAMCA6gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
BwMCMB0GA1UdDgQWBBSkX2YHZbQnx3LV6xPWQoLCraUJLjAfBgNVHSMEGDAWgBQo
jYFoRyc8k2rB3kC+QSJEhYep5zBBBggrBgEFBQcBAQQ1MDMwMQYIKwYBBQUHMAKG
JWh0dHA6Ly92YXVsdC52YXVsdC5zdmM6ODIwMC92MS9wa2kvY2EwGgYDVR0RBBMw
EYIPd3d3LmV4YW1wbGUuY29tMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly92YXVs
dC52YXVsdC5zdmM6ODIwMC92MS9wa2kvY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQBs
YLfkpcWF4dTLKa/teEQhw25JlYhs3BkHlguoMVEuQ/oH7XxVPhMBOsVwhp2JQ3m0
tAQtP3n2Di/4cY0snVbuz8Iq3gmg0MDEHpWEcrwKtlqZ2AjhzNRyj0d+0CFNQWsu
/wDIuYeBGylG4p4vprMv+ColGHp5QVNNtsghxVPpxKKxZICYW9qmrsl+Z2P0OF5L
AtUnM6ZgPKB4B8feRJ6B+dl5ipo2KkMl1VYASGNVfpbu+CXFq8ZHntXk73pbgJfJ
z6cSNZfKKqwd2WrA7kqdHX2bzDiHTPGBSVOIDka3xMzggGX5ExA7VaWWyju4KcFD
bAq/fzlTKoAaHWdPwqUU
-----END CERTIFICATE-----
'
tls.key='-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBA...
-----END RSA PRIVATE KEY-----
'
token='eyJh...'
#
echo "" | openssl x509 -noout -text
...
Issuer: CN=example.com
Validity
Not Before: Nov 30 05:06:55 2025 GMT
Not After : Dec 3 05:07:25 2025 GMT
Subject: CN=www.example.com
...
X509v3 extensions:
X509v3 Key Usage: critical
Digital Signature, Key Encipherment, Key Agreement
X509v3 Extended Key Usage:
TLS Web Server Authentication, TLS Web Client Authentication
X509v3 Subject Key Identifier:
A4:5F:66:07:65:B4:27:C7:72:D5:EB:13:D6:42:82:C2:AD:A5:09:2E
X509v3 Authority Key Identifier:
28:8D:81:68:47:27:3C:93:6A:C1:DE:40:BE:41:22:44:85:87:A9:E7
Authority Information Access:
CA Issuers - URI:http://vault.vault.svc:8200/v1/pki/ca
X509v3 Subject Alternative Name:
DNS:www.example.com
X509v3 CRL Distribution Points:
Full Name:
URI:http://vault.vault.svc:8200/v1/pki/crl