[Vault란?]
Docs: https://developer.hashicorp.com/vault/docs/what-is-vault
HashiCorp Vault는 신원 기반(identity-based)의 시크릿 및 암호화 관리 시스템으로, 민감한 데이터를 안전하게 관리하고 보호하기 위한 보안 도구로, 비밀번호, API 키, 인증서 같은 중요 정보를 중앙화된 시스템에서 안전하게 저장하고 관리한다.
Vault는 저장 데이터와 전송 데이터 모두에 대해 암호화를 제공하며, 애플리케이션이 필요로 하는 암호화 서비스도 지원한다. 필요 시점에 일회성 자격 증명을 동적으로 생성할 수도 있으며 세밀한 정책 기반 접근 제어 시스템을 갖추고 있어 누가 어떤 비밀에 접근할 수 있는지에 대한 접근 제어도 가능하다(인가).
또한 클라우드 플랫폼, 데이터베이스, SSH 등 다양한 시스템의 자격 증명을 관리하고 롤링을 지원하며, 클라우드 자격 증명 보호, 데이터베이스 접근 관리, PKI 관리, 암호화 서비스 제공, 마이크로서비스 간 안전한 통신, DevOps 및 CI/CD 파이프라인 보안 등 다양한 곳에서 사용할 수 있다. k8s와 마찬가지로 API 기반으로 작동한다.
[대표적인 시크릿 종류]
[Vault의 동작 방식]
[Vault의 사용 사례]
(Vault 활용 및 통합사례의 발표자료 중 캡처)
호텔 체크인 과정 🔑
1. 인증 및 카드키 신청: 호텔 투숙객이 신분증/여권을 통해 신원 확인 후 카드키를 신청한다.
2. 카드키 정책 등록: 리셉션에서 다음 정보를 기반으로 정책을 등록한다
- 1차: 엘리베이터 2층
- 2차: 객실 208호
- 기간: 3일
- 카드키 발급: 투숙객에게 카드키가 발급된다.
- 출입: 투숙객은 발급받은 카드키로 객실에 임시 출입할 수 있다.
=> "카드키는 객실에 대한 임시 출입 허가"를 나타낸다.
Vault를 통한 클라우드 접근 절차 🚙
1. 인증 및 접근키 신청: 사용자/앱이 다양한 인증 시스템(Okta, LDAP, TLS, Azure 등)을 통해 접근키를 신청한다.
2. 접근키 생성: HashiCorp Vault에서 다음 정보를 기반으로 접근키를 생성한다
- AWS 접근 권한
- 대상 서비스: S3
- 기간: 1일
- 접근자 위치: x.x.x.0/24 (특정 IP 대역)
- 접근키 발급: 사용자/앱에 접근키가 발급된다.
- 접근: 사용자/앱은 발급받은 키로 클라우드 서비스(AWS, Google Cloud, Azure 등)에 접근할 수 있게 된다.
=> "Vault는 클라우드에 접근할 수 있는 임시 접근키를 발급"함을 나타낸다.
[Vault 내부 아키텍처]
Docs: https://developer.hashicorp.com/vault/docs/internals/architecture
# Create a Kubernetes namespace.
kubectl create namespace vault
# View all resources in a namespace.
kubectl get all --namespace vault
# Setup Helm repo
helm repo add hashicorp https://helm.releases.hashicorp.com
# Check that you have access to the chart.
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
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
disable_mlock = true
cluster_name = "vault-local"
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_disable = 1
}
storage "raft" { # Raft 구성 권장
path = "/vault/data"
node_id = "vault-dev-node-1"
}
service:
enabled: true
type: NodePort
port: 8200
targetPort: 8200
nodePort: 30000 # Kind에서 열어둔 포트 중 하나 사용
injector:
enabled: true
ui:
enabled: true
serviceType: "NodePort"
EOF
helm install을 실행한 뒤 제대로 설치가 되었는지 확인한다.
# Helm Install 실행
helm upgrade vault hashicorp/vault -n vault -f override-values.yaml --install
# 네임스페이스 변경 : vault
kubens vault
Context "kind-myk8s" modified.
Active namespace is "vault".
# 배포확인
k get pods,svc,pvc
제대로 설치가 된 것을 확인할 수 있다. 이제 Vault 초기화 및 잠금해제를 진행해본다.
kubectl exec -ti vault-0 -- vault status
Seal Type: shamir - Vault가 Shamir 알고리즘을 사용하여 seal 키를 관리하고 있다.
Initialized: false - Vault가 아직 초기화되지 않았다.
Sealed: true - Vault가 현재 봉인된 상태이다.
Total Shares: 0 및 Threshold: 0 - 아직 초기화되지 않았기 때문에 키 공유 수와 임계값이 설정되지 않았다.
Unseal Progress: 0/0 - 봉인 해제 진행 상황이 없다.
Version: 1.19.0 - 실행 중인 Vault의 버전
Storage Type: raft - 데이터 저장소로 Raft 합의 알고리즘 기반 스토리지를 사용 중이다.
HA Enabled: true - 고가용성(High Availability) 기능이 활성화되어 있다.
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를 실행한 결과이다. 실행이 완료되면 [Vault Unsealed!] 메세지를 확인할 수 있다.
kubectl exec -ti vault-0 -- vault status
실행한 이후에는 몇 가지 값들이 바뀐 것을 확인할 수 있다.
Seal Type: shamir - Vault가 Shamir 비밀 공유 알고리즘을 사용하여 봉인(seal) 키를 관리한다.
Initialized: true - Vault가 성공적으로 초기화되었다.
Sealed: false - Vault가 봉인 해제(unsealed) 상태입니다. 이는 Vault가 활성화되어 저장된 비밀에 접근할 수 있게 된다.
Total Shares: 1 - 봉인 키가 1개의 조각으로 나눠져 있습니다. (단일 키로 관리)
Threshold: 1 - 봉인 해제에 필요한 키 조각의 수 (단일 키)
Version: 1.19.0 - 실행 중인 Vault의 버전
Storage Type: raft - 데이터 저장소로 Raft 합의 알고리즘 기반 스토리지를 사용 중이다.
Cluster Name: vault-local - Vault 클러스터의 이름
Cluster ID: 클러스터의 고유 식별자입니다.
HA Enabled: true - 고가용성(High Availability) 기능이 활성화되어 있다.
HA Cluster: https://vault-0.vault-internal:8201 - HA 클러스터의 내부 통신 URL
HA Mode: active - 이 Vault 인스턴스가 현재 활성(active) 상태로 작동 중이다.
Raft Committed/Applied Index: 37 - Raft 합의 알고리즘으로 적용된 인덱스 번호
포트포워딩 이후에 UI에 접근도 가능하다. 실습을 위해 KV 엔진 활성화 및 샘플 데이터를 추가한다.
# KV v2 형태로 엔진 활성화
vault secrets enable -path=secret kv-v2
# 샘플 시크릿 저장
vault kv put secret/sampleapp/config \
username="demo" \
password="p@ssw0rd"
# 입력된 데이터 확인
vault kv get secret/sampleapp/config
======== Secret Path ========
secret/data/sampleapp/config
======= Metadata =======
Key Value
--- -----
created_time 2025-03-30T05:18:47.797852422Z
custom_metadata <nil>
deletion_time n/a
destroyed false
version 1
====== Data ======
Key Value
--- -----
password p@ssw0rd
username demo
이후에 UI에서 확인하면 Version 1로 설정한 password와 username을 확인할 수 있다.
Vault Agent Injector는 Kubernetes Pod 내부에 Vault Agent를 자동으로 주입해주는 기능이다. 이를 통해 어플리케이션이 Vault로부터 자동으로 비밀 정보를 받아올 수 있게 된다.
이 기능을 사용하기 위한 제약 조건
1. Vault가 설치되어 있고, Kubernetes와 통합되어 있어야 한다
2. Vault Agent Injector가 클러스터에 배포되어 있어야 한다 (별도의 구성요소)
3. Kubernetes 인증 방식이 활성화되어야 한다
4. 정책과 역할이 정의되어 있어야 한다
5. 애플리케이션 Pod에 주입할 주석(annotation
)을 추가해야 한다 (특정 주석이 있는 Pod에 대해서만 Vault Agent를 주입하기 때문)
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "example-role"
[실습]
Vault AppRole 방식 인증 구성해본다.
# 1. AppRole 인증 방식 활성화
vault auth enable approle || echo "AppRole already enabled"
vault auth list
approle이 추가된 것을 확인할 수 있다.
# 2. 정책 생성
vault policy write sampleapp-policy - <<EOF
path "secret/data/sampleapp/*" {
capabilities = ["read"]
}
EOF
# 3. AppRole Role 생성
vault write auth/approle/role/sampleapp-role \
token_policies="sampleapp-policy" \
secret_id_ttl="1h" \
token_ttl="1h" \
token_max_ttl="4h"
# 4. Role ID 및 Secret ID 추출 및 저장
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"
# 5. 파일로 저장
mkdir -p approle-creds
echo "$ROLE_ID" > approle-creds/role_id.txt
echo "$SECRET_ID" > approle-creds/secret_id.txt
그 뒤에는 Vault Agent Sidecar를 연동한다. Vault Agent는 vault-agent-config.hcl 설정을 통해 연결할 Vault의 정보와, Template 구성, 렌더링 주기, 참조할 Vault KV 위치정보 등을 정의한다.
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
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
kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: NodePort
selector:
app: nginx-vault-demo
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30001 # Kind에서 설정한 Port
EOF
kubectl get pod -l app=nginx-vault-demo
kubectl describe pod -l app=nginx-vault-demo
파드 내에 사이드카 컨테이너 추가되어 2/2가 된 것을 확인할 수 있다.
# 볼륨 마운트 확인
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- ls -l /etc/vault-agent-token
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/vault-agent-token/token ; echo
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- ls -al /etc/vault
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/vault/agent-config.hcl
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- ls -al /etc/vault/approle
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/vault/approle/role_id ; echo
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/vault/approle/secret_id ; echo
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- ls -l /etc/secrets
total 4
-rw-r--r-- 1 vault vault 94 Apr 10 02:09 index.html
# mutating admission
kubectl get mutatingwebhookconfigurations.admissionregistration.k8s.io
NAME WEBHOOKS AGE
vault-agent-injector-cfg 1 3h10m
그 뒤에 vault ui에서 password를 변경하고 nginx에서 다시 확인한다.
kubectl exec -it deploy/nginx-vault-demo -c vault-agent-sidecar -- cat /etc/secrets/index.html
*회사 컴퓨터에서 SSL 통신이 차단되어 Jenkins 설치가 제대로 안되어서 실습을 별도로 진행하지는 않았다.
젠킨스는 Vault에 시크릿으로 분류된 데이터를 필요로 하는 작업(job)을 실행한다.
젠킨스는 마스터 노드와 워커 노드를 가지고 있으며, 워커 노드는 짧은 시간 동안 실행되는 컨테이너 러너에서 작업을 실행한다.
pipeline {
agent any
environment {
VAULT_ADDR = 'http://192.168.0.2:30000' // 실제 Vault 주소로 변경
}
stages {
stage('Read Vault Secret') {
steps {
withVault([
vaultSecrets: [
[
path: 'secret/sampleapp/config',
engineVersion: 2,
secretValues: [
[envVar: 'USERNAME', vaultKey: 'username'],
[envVar: 'PASSWORD', vaultKey: 'password']
]
]
],
configuration: [
vaultUrl: "${VAULT_ADDR}",
vaultCredentialId: 'vault-approle-creds'
]
]) {
sh '''
echo "Username from Vault: $USERNAME"
echo "Password from Vault: $PASSWORD"
'''
script {
echo "Username (env): ${env.USERNAME}"
echo "Password (env): ${env.PASSWORD}"
}
}
}
}
}
}
📖 Vault + ArgoCD Plugin 패턴