안녕하세요?
AEWS 10주차 과제로 Vault를 가지고 하는 K8S Secret 관리를 작성해보겠습니다.
Vault
Vault가 뭔가요?
HashiCorp Vault는 신원 기반(identity-based)의 시크릿 및 암호화 관리 시스템입니다. 이 시스템은 인증(authentication) 및 인가(authorization) 방법을 통해 암호화 서비스를 제공하여 비밀에 대한 안전하고 감사 가능하며 제한된 접근을 보장합니다.
시크릿(Secret)이란 접근을 철저히 통제하고자 하는 모든 것을 의미하며, 예를 들어 토큰, API 키, 비밀번호, 암호화 키 또는 인증서 등이 이에 해당합니다. Vault는 모든 시크릿에 대해 통합된 인터페이스를 제공하면서, 엄격한 접근 제어와 상세한 감사 로그 기록 기능을 제공합니다.
외부 서비스용 API 키, 서비스 지향 아키텍처 간 통신을 위한 자격 증명 등은 플랫폼에 따라 누가 어떤 비밀에 접근했는지를 파악하기 어려울 수 있습니다. 여기에 키 롤링(교체), 안전한 저장, 상세한 감사 로그까지 추가하려면 별도의 커스텀 솔루션 없이는 거의 불가능합니다. Vault는 바로 이 지점에서 해결책을 제공합니다.
Vault는 클라이언트(사용자, 기계, 애플리케이션 등)를 검증하고 인가한 후에만 비밀이나 저장된 민감한 데이터에 접근할 수 있도록 합니다.
Vault의 동작 방식
Vault의 핵심 워크플로우는 다음 네 단계로 구성됩니다:
- 인증 (Authenticate): Vault에서 인증은 클라이언트가 Vault에 자신이 누구인지 증명할 수 있는 정보를 제공하는 과정입니다. 클라이언트가 인증 메서드를 통해 인증되면, 토큰이 생성되고 정책과 연결됩니다.
- 검증 (Validation): Vault는 Github, LDAP, AppRole 등과 같은 신뢰할 수 있는 외부 소스를 통해 클라이언트를 검증합니다.
- 인가 (Authorize): 클라이언트는 Vault의 보안 정책과 비교됩니다. 이 정책은 Vault 토큰을 사용하여 클라이언트가 접근할 수 있는 API 엔드포인트를 정의하는 규칙의 집합입니다. 정책은 Vault 내 특정 경로나 작업에 대한 접근을 허용하거나 거부하는 선언적 방식으로 권한을 제어합니다.
- 접근 (Access): Vault는 클라이언트의 신원에 연관된 정책을 기반으로 토큰을 발급하여 비밀, 키, 암호화 기능 등에 대한 접근을 허용합니다. 클라이언트는 이후 작업에서 해당 Vault 토큰을 사용할 수 있습니다.
Vault의 기본 구조는 아래와 같이 호텔 체크인에 비유할 수 있습니다.
테스트 환경 구성
이 실습은 로컬 MacOS 환경에서 Kind로 생성한 K8S 클러스터에서 진행합니다.
Kubernetes 세팅
kind로 배포할 config를 준비합니다.
cat > kind-3node.yaml <<EOF
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
apiServerAddress: "127.0.0.1" # $MyIP로 설정하셔도 됩니다.
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30000
hostPort: 30000
- containerPort: 30001
hostPort: 30001
- containerPort: 30002
hostPort: 30002
- containerPort: 30003
hostPort: 30003
- containerPort: 30004
hostPort: 30004
- containerPort: 30005
hostPort: 30005
- containerPort: 30006
hostPort: 30006
- role: worker
- role: worker
EOF
클러스터를 생성하겠습니다.
kind create cluster --config kind-3node.yaml --name myk8s --image kindest/node:v1.32.2
노드를 확인하면 control-plane과 2개의 worker가 생성되어 있는 것을 확인할 수 있습니다.
kind get nodes --name myk8s
myk8s-worker2
myk8s-worker
myk8s-control-plane
kubens default
Context "kind-myk8s" modified.
Active namespace is "default".
생성된 클러스터의 네트워크도 확인해보겠습니다.
docker network ls
docker inspect kind | jq
[
{
"Name": "kind",
"Id": "7a90578a3a56ec00898c824fa6b45706e85d8e5e64a9d489d2742e9c7bb580b7",
"Created": "2024-09-01T20:16:36.120864157+09:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": true,
"IPAM": {
"Driver": "default",
"Options": {},
"Config": [
{
"Subnet": "192.168.247.0/24",
"Gateway": "192.168.247.1"
},
{
"Subnet": "fc00:f853:ccd:e793::/64",
"Gateway": "fc00:f853:ccd:e793::1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {
"1ab01ce58e995b550c89405fde98a5bf9a5625d64dee96781dd81856133de716": {
"Name": "myk8s-worker",
"EndpointID": "7031779b432353a69779cee1231afc3ad7477faf471073c33faa019094448f3d",
"MacAddress": "02:42:c0:a8:f7:04",
"IPv4Address": "192.168.247.4/24",
"IPv6Address": "fc00:f853:ccd:e793::4/64"
},
...
}
},
"Options": {
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
}
]
kubectl get node -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
myk8s-control-plane Ready control-plane 15m v1.32.2 192.168.247.2 <none> Debian GNU/Linux 12 (bookworm) 6.12.15-orbstack-00304-gd0ddcf70447d containerd://2.0.3
myk8s-worker Ready <none> 15m v1.32.2 192.168.247.4 <none> Debian GNU/Linux 12 (bookworm) 6.12.15-orbstack-00304-gd0ddcf70447d containerd://2.0.3
myk8s-worker2 Ready <none> 15m v1.32.2 192.168.247.3 <none> Debian GNU/Linux 12 (bookworm) 6.12.15-orbstack-00304-gd0ddcf70447d containerd://2.0.3
Vault 세팅
이제 Vault를 설치해보겠습니다.
kubectl create namespace vault
helm repo add hashicorp https://helm.releases.hashicorp.com
helm search repo hashicorp/vault
NAME CHART VERSION APP VERSION DESCRIPTION
hashicorp/vault 0.30.0 1.19.0 Official HashiCorp Vault Chart
hashicorp/vault-secrets-gateway 0.0.2 0.1.0 A Helm chart for Kubernetes
hashicorp/vault-secrets-operator 0.10.0 0.10.0 Official Vault Secrets Operator Chart
Valult의 helm values 설정을 아래와 같이 미리 준비합니다.
cat <<EOF > override-values.yaml
global:
enabled: true
tlsDisable: true # Disable TLS for demo purposes
server:
image:
repository: "hashicorp/vault"
tag: "1.19.0"
standalone:
enabled: true
replicas: 1
config: |
ui = true
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_disable = 1
}
storage "file" {
path = "/vault/data"
}
service:
enabled: true
type: NodePort
port: 8200
targetPort: 8200
nodePort: 30000 # 🔥 Kind에서 열어둔 포트 중 하나 사용
injector:
enabled: true
EOF
준비한 설정대로 Vault를 Helm으로 설치합니다.
helm upgrade vault hashicorp/vault -n vault -f override-values.yaml --install
k get pods,svc,pvc -n vault
NAME READY STATUS RESTARTS AGE
pod/vault-0 0/1 Running 0 30s
pod/vault-agent-injector-56459c7545-nrk4t 1/1 Running 0 30s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/vault NodePort 10.96.59.63 <none> 8200:30000/TCP,8201:31683/TCP 30s
service/vault-agent-injector-svc ClusterIP 10.96.136.142 <none> 443/TCP 30s
service/vault-internal ClusterIP None <none> 8200/TCP,8201/TCP 30s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/data-vault-0 Bound pvc-27ffba74-3541-4170-ae77-3df544fac62a 10Gi RWO standard <unset> 30s
Vault의 상태를 확인해 봅니다.
ubectl -n vault exec -ti vault-0 -- vault status
Key Value
--- -----
Seal Type shamir
Initialized false
Sealed true
Total Shares 0
Threshold 0
Unseal Progress 0/0
Unseal Nonce n/a
Version 1.19.0
Build Date 2025-03-04T12:36:40Z
Storage Type file
HA Enabled false
init-unseal.sh 이라는 파일을 하나 만들어서 Vault Unseal 자동화할 수 있도록 해봅니다.
cat <<EOF > init-unseal.sh
#!/bin/bash
# Vault Pod 이름
VAULT_POD="vault-0"
# Vault 명령 실행
VAULT_CMD="kubectl exec -ti \$VAULT_POD -- vault"
# 출력 저장 파일
VAULT_KEYS_FILE="./vault-keys.txt"
UNSEAL_KEY_FILE="./vault-unseal-key.txt"
ROOT_TOKEN_FILE="./vault-root-token.txt"
# Vault 초기화 (Unseal Key 1개만 생성되도록 설정)
\$VAULT_CMD operator init -key-shares=1 -key-threshold=1 | sed \$'s/\\x1b\\[[0-9;]*m//g' | tr -d '\r' > "\$VAULT_KEYS_FILE"
# Unseal Key / Root Token 추출
grep 'Unseal Key 1:' "\$VAULT_KEYS_FILE" | awk -F': ' '{print \$2}' > "\$UNSEAL_KEY_FILE"
grep 'Initial Root Token:' "\$VAULT_KEYS_FILE" | awk -F': ' '{print \$2}' > "\$ROOT_TOKEN_FILE"
# Unseal 수행
UNSEAL_KEY=\$(cat "\$UNSEAL_KEY_FILE")
\$VAULT_CMD operator unseal "\$UNSEAL_KEY"
# 결과 출력
echo "[🔓] Vault Unsealed!"
echo "[🔐] Root Token: \$(cat \$ROOT_TOKEN_FILE)"
EOF
# 실행 권한 부여
chmod +x init-unseal.sh
# 실행
./init-unseal.sh
파일을 실행하면..
./init-unseal.sh
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 file
Cluster Name vault-cluster-3d859970
Cluster ID ad7a59aa-f76e-aad0-cc4e-d98f7e192106
HA Enabled false
[🔓] Vault Unsealed!
[🔐] Root Token: ---
그전에 Sealed라고 되어있던 부분이 true -> false로 바뀐 모습을 확인할 수 있습니다.
이제 Unseal이 마무리 되었으므로 Root Token 값을 사용하여 UI에 접속할 수 있습니다.
Vault CLI 설정
Vault를 CLI로 사용할 수 있도록 세팅해보겠습니다.
brew tap hashicorp/tap
brew install hashicorp/tap/vault
vault --version
Vault v1.19.1 (aa75903ec499b2236da9e7bbbfeb7fd16fa4fd9d), built 2025-04-02T15:43:01Z
# NodePort로 공개한 30000 Port로 설정
export VAULT_ADDR='http://localhost:30000'
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 file
Cluster Name vault-cluster-3d859970
Cluster ID ad7a59aa-f76e-aad0-cc4e-d98f7e192106
HA Enabled false
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 ---
token_accessor ---
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
테스트
KV 시크릿 엔진 활성화 및 샘플 구성
Vault KV version 2 엔진을 활성화하고 샘플 데이터를 저장합니다.
Version1 : KV 버전관리 불가 / Version2 : KV 버전관리 가능
KV v2 형태로 엔진 활성화합니다.
vault secrets enable -path=secret kv-v2
Success! Enabled the kv-v2 secrets engine at: secret/
그리고 샘플 시크릿을 저장해봅니다.
vault kv put secret/sampleapp/config \
username="demo" \
password="p@ssw0rd"
======== Secret Path ========
secret/data/sampleapp/config
======= Metadata =======
Key Value
--- -----
created_time 2025-04-10T11:02:57.027681868Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
입력된 데이터를 확인해봅니다.
vault kv get secret/sampleapp/config
======== Secret Path ========
secret/data/sampleapp/config
======= Metadata =======
Key Value
--- -----
created_time 2025-04-10T11:02:57.027681868Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
====== Data ======
Key Value
--- -----
password p@ssw0rd
username demo
실제 Vault의 UI에서도 해당 시크릿을 확인할 수 있습니다.
Vault Sidecar (Vault Agent)연동
Vault Agent Sidecar 패턴을 이용해서 Pod가 Secret을 Volume으로 해서 마운트하도록 구성합니다.
Vault Sidecar 아키텍쳐는 아래와 같습니다.
Vault Agent Injector는 Kubernetes Pod 내부에 Vault Agent를 자동으로 주입해주는 기능입니다.
이를 통해 어플리케이션이 Vault로부터 자동으로 시크릿를 받아올 수 있게 됩니다.
먼저 AppRole 인증 방식을 활성화합니다.
vault auth enable approle || echo "AppRole already enabled"
Success! Enabled approle auth method at: approle/
vault auth list
Path Type Accessor Description Version
---- ---- -------- ----------- -------
approle/ approle auth_approle_0e9fad54 n/a n/a
token/ token auth_token_8a73a32a token based credentials n/a
샘플용 정책을 생성합니다.
- secret/data/sampleapp/* 경로에 대해 read 권한만 부여합니다.
- 이 정책은 특정 경로의 시크릿만 읽을 수 있도록 제한합니다
vault policy write sampleapp-policy - <<EOF
path "secret/data/sampleapp/*" {
capabilities = ["read"]
}
EOF
Success! Uploaded policy: sampleapp-policy
- sampleapp-role이라는 AppRole을 생성합니다.
- 해당 역할로 발급된 토큰은 sampleapp-policy 권한을 갖습니다.
- secret_id는 1시간만 유효, 발급된 토큰은 1시간 TTL, 최대 4시간 TTL.
vault write auth/approle/role/sampleapp-role \
token_policies="sampleapp-policy" \
secret_id_ttl="1h" \
token_ttl="1h" \
token_max_ttl="4h"
Success! Data written to: auth/approle/role/sampleapp-role
- 위에서 만든 AppRole에 대한 ROLE_ID와 SECRET_ID를 추출합니다.
- 이는 Vault Agent가 인증할 때 사용할 자격 정보입니다.
ROLE_ID=$(vault read -field=role_id auth/approle/role/sampleapp-role/role-id)
SECRET_ID=$(vault write -f -field=secret_id auth/approle/role/sampleapp-role/secret-id)
echo "ROLE_ID: $ROLE_ID"
echo "SECRET_ID: $SECRET_ID"
ROLE_ID: c2d5513f-2a53-ceea-6b43-0afbbc389315
SECRET_ID: 6475069b-7de1-93fc-ce4f-4cb236060a58
파일로 저장합니다.
mkdir -p approle-creds
echo "$ROLE_ID" > approle-creds/role_id.txt
echo "$SECRET_ID" > approle-creds/secret_id.txt
그리고 Kubernetes Secret으로 저장합니다.
kubectl create secret generic vault-approle -n vault \
--from-literal=role_id="${ROLE_ID}" \
--from-literal=secret_id="${SECRET_ID}" \
--save-config \
--dry-run=client -o yaml | kubectl apply -f -
이
제 Vault Agent를 설정할 준비를 합니다.
vault-agent-config.hcl 설정을 통해 연결할 Vault의 정보와 Template 구성, 렌더링 주기, 참조할 Vault KV 위치정보 등을 정의합니다.
Vault Agent의 구성 파일(agent-config.hcl)을 Kubernetes ConfigMap으로 생성하고 적용합니다.
해당 내용 중에는
- Vault 서버 주소를 지정합니다. (vault 네임스페이스의 서비스 "http://vault.vault.svc:8200")
- Vault Agent가 /etc/vault/approle 경로에서 RoleID/SecretID 파일을 읽어 자동 인증합니다.
- 인증된 결과 토큰은 /etc/vault-agent-token/token에 저장됩니다.
- Vault에서 시크릿을 주기적으로 조회하고 템플릿을 렌더링합니다.
- 이 경우 HTML 파일 /etc/secrets/index.html을 만들어서 시크릿 내용을 넣습니다.
cat <<EOF | kubectl create configmap vault-agent-config -n vault --from-file=agent-config.hcl=/dev/stdin --dry-run=client -o yaml | kubectl apply -f -
vault {
address = "http://vault.vault.svc:8200"
}
auto_auth {
method "approle" {
config = {
role_id_file_path = "/etc/vault/approle/role_id"
secret_id_file_path = "/etc/vault/approle/secret_id"
remove_secret_id_file_after_reading = false
}
}
sink "file" {
config = {
path = "/etc/vault-agent-token/token"
}
}
}
template_config {
static_secret_render_interval = "20s"
}
template {
destination = "/etc/secrets/index.html"
contents = <<EOH
<html>
<body>
<p>username: {{ with secret "secret/data/sampleapp/config" }}{{ .Data.data.username }}{{ end }}</p>
<p>password: {{ with secret "secret/data/sampleapp/config" }}{{ .Data.data.password }}{{ end }}</p>
</body>
</html>
EOH
}
EOF
샘플 애플리케이션으로서 Nginx와 Sidecar로서 vault를 배포합니다.
kubectl apply -n vault -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-vault-demo
spec:
replicas: 1
selector:
matchLabels:
app: nginx-vault-demo
template:
metadata:
labels:
app: nginx-vault-demo
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
volumeMounts:
- name: html-volume
mountPath: /usr/share/nginx/html
- name: vault-agent-sidecar
image: hashicorp/vault:latest
args:
- "agent"
- "-config=/etc/vault/agent-config.hcl"
volumeMounts:
- name: vault-agent-config
mountPath: /etc/vault
- name: vault-approle
mountPath: /etc/vault/approle
- name: vault-token
mountPath: /etc/vault-agent-token
- name: html-volume
mountPath: /etc/secrets
volumes:
- name: vault-agent-config
configMap:
name: vault-agent-config
- name: vault-approle
secret:
secretName: vault-approle
- name: vault-token
emptyDir: {}
- name: html-volume
emptyDir: {}
EOF
해당 Nginx Pod를 확인해보면 Vault Sidecar가 들어있는 모습을 볼 수 있습니다.
볼륨 마운트가 되어있는지 확인합니다.
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- ls -l /etc/vault-agent-token
total 4
-rw-r----- 1 vault vault 95 Apr 10 11:22 token
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/vault-agent-token/token ; echo
hvs.CAESID7SBntL_0-7sJamhidUyA50aexVjFwzeqpPxO0zu0zyGh4KHGh2cy5uOXBQQzZDdjhVVzJZbWhYZXNZU3pzMlQ
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/secrets/index.html
<html>
<body>
<p>username: demo</p>
<p>password: p@ssw0rd</p>
</body>
</html>
실제 배포된 화면을 확인해보겠습니다.
'Kubernetes' 카테고리의 다른 글
Gateway API, AWS Gateway API Controller (0) | 2025.04.26 |
---|---|
EKS Blue/Green Migration (0) | 2025.04.05 |
Kubernetes Scheduling과 Scheduling plugins 파보기 (0) | 2025.03.20 |
Karpenter + Keda로 특정 시간에 Autoscaling 걸기 (0) | 2025.03.16 |
External Secret으로 AWS Secrets Manager와 EKS Secret 동기화하기 (0) | 2025.03.15 |