Tạo và lưu secret trong cụm K8s luôn tiềm ẩn những rủi ro, vì thế, việc xây dựng 1 hệ thống lưu trữ secrets riêng là hoàn toàn phù hợp trong PROD.
VAULT chính là 1 lựa chọn hợp lý:
Mô hình này được áp dụng ở rất nhiều công ty lớn, mình từng làm bank như TCB, MSB, và cũng đang dùng mô hình tương tự.
Làm sao để các deployment có thể sử dụng đc secrets lưu tại Vault? Làm sao để tích hợp Vault với K8s?
Mình sẽ có 1 demo như sau :
- Vẫn là 1 app gen random data vào Mysql.
- Trong code, có chỉ định sẽ dùng env để chỉ định là sử dụng host, mysql password…
- Thay vì tạo secret như cách thông thường, thì ta sẽ lưu các secret vào Vault, và tích hợp với K8s.
Cách thực hiện :
- Triển khai Vault server 10.100.1.104
#Install vault
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install vault -y
- Tạo cấu hình single-node (Raft storage)
sudo mkdir -p /opt/vault/data
sudo chown -R vault:vault /opt/vault sudo tee /etc/vault.d/vault.hcl >/dev/null <<'EOF'
ui = true
disable_mlock = true storage "raft" { path = "/opt/vault/data" node_id = "vault-1"
} listener "tcp" { address = "0.0.0.0:8200" tls_disable = 1 # Lab nhanh; Production nên dùng TLS (ghi chú ở cuối)
} api_addr = "http://10.100.1.104:8200"
cluster_addr = "http://10.100.1.104:8201"
EOF sudo systemctl enable vault
sudo systemctl start vault
sleep 30
sudo sysremctl status vault
- Khởi tạo & unseal
mkdir vault && cd vault
echo 'export VAULT_ADDR=http://10.100.1.104:8200' | sudo tee -a /etc/profile.d/vault.sh
source /etc/profile.d/vault.sh vault operator init -key-shares=1 -key-threshold=1 > ~/vault/vault.init
grep 'Unseal Key 1' ~/vault/vault.init | awk '{print $4}' > ~/vault/unseal.key
grep 'Initial Root Token' ~/vault/vault.init | awk '{print $4}' > ~/vault/root.token vault operator unseal "$(cat ~/vault/unseal.key)"
vault login "$(cat ~/vault/root.token)"
#Sau khi login, se hien thong tin dang nhap Vault qua token, vi du nhu sau :
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.20.2
Build Date 2025-08-05T19:05:39Z
Storage Type raft
Cluster Name vault-cluster-33700384
Cluster ID b5908982-4821-007d-1577-7c12aa2339d5
Removed From Cluster false
HA Enabled true
HA Cluster https://10.100.1.104:8201
HA Mode active
Active Since 2025-08-15T03:53:48.995498791Z
Raft Committed Index 37
Raft Applied Index 37
wasadm@databases-server:~/vault$ vault login "$(cat ~/vault/root.token)"
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token. Key Value
--- -----
token hvs.vXLK7LgQZuEgGVNbgMsfB2Q0
token_accessor d1nMP5A81wOvCJiJFV5P0UkR
token_duration ∞
token_renewable false
token_policies ["root"]
identity_policies []
policies ["root"]
Dùng token : hvs.vXLK7LgQZuEgGVNbgMsfB2Q0 để login vault
Url : http://10.100.1.104:8200
- Bật audit
sudo mkdir -p /var/log/vault && sudo chown vault:vault /var/log/vault
vault audit enable file file_path=/var/log/vault/audit.log
- Tạo KV v2
vault secrets enable -path=kv kv-v2
- Kết nối Vault ↔ Kubernetes (Kubernetes Auth)
Thực hiện trên con master : 10.100.1.120
mkdir rbac-vault && cd rbac-vault
Tạo ServiceAccount cho Vault dùng để review token:
vim vault-auth-sa.yaml ---
apiVersion: v1
kind: ServiceAccount
metadata: name: vault-auth namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata: name: vault-auth-delegator
roleRef: kind: ClusterRole name: system:auth-delegator apiGroup: rbac.authorization.k8s.io
subjects: - kind: ServiceAccount name: vault-auth namespace: kube-system ---
kubectl apply -f vault-auth-sa.yaml
Lấy Token reviewer và CA của API server (Trên con master)
SA_TOKEN=$(kubectl create token vault-auth -n kube-system --duration=24h)
echo $SA_TOKEN
Copy SA_TOKEN sang Vault server :
export SA_TOKEN=gia tri cua SA_TOKEN
Copy content của ca.crt trong /etc/kubernetes/pki/ca.crt sang /home/wasadm/vault/k8s-ca.crt
- Cấu hình auth trong Vault (chạy trên 10.100.1.104)
vault auth enable kubernetes vault write auth/kubernetes/config \ token_reviewer_jwt="$SA_TOKEN" \ kubernetes_host="https://10.100.1.120:6443" \ kubernetes_ca_cert=@/home/wasadm/vault/k8s-ca.crt
- Tạo secret path và policy chỉ cho phép đọc đúng path
vault kv put demo-db-secrets/demo/db-secret mysql-root-password="Tech@1604" mysql-user="etl_user" mysql-password="123456"
cd /home/wasadm/vault
mkdir demo-ns && cd demo-ns
vim demo-app.hcl ---
path "demo-db-secrets/data/demo/*" { capabilities = ["read"]
}
# Cho phép đọc metadata KV v2 (ESO thường cần để xử lý versioning)
path "demo-db-secrets/metadata/demo/*" { capabilities = ["read"]
} vault policy write demo-app demo-app.hcl
- Tạo role ràng buộc Namespace + ServiceAccount của app
vault write auth/kubernetes/role/demo-app bound_service_account_names="demo-app" bound_service_account_namespaces="demo" policies="demo-app" ttl="300h"
Bây giờ dừng lại 1 chút, để chúng ta xem 1 vài khái niệm trong Vault:
Làm đến đây, sẽ có nhiều bạn thắc mắc : unseal là gì? bật audit để làm gì?
Unseal trong Vault là gì?
- Vault lưu trữ dữ liệu (secret, token, policy...) ở backend storage (VD: Raft, Consul) ở dạng mã hóa.
- Mỗi khi Vault khởi động lại (restart, crash, upgrade…), nó ở trạng thái sealed — tức là dữ liệu vẫn được mã hóa, chưa thể đọc/ghi.
- Để “mở khóa” dữ liệu, bạn phải unseal Vault bằng Unseal Key (có thể chia thành nhiều key, yêu cầu đủ
key-threshold
mới mở được).
Cơ chế bên trong:
- Khi init, Vault tạo một Master Key để giải mã dữ liệu.
- Master Key không được lưu thẳng, mà chia nhỏ (Shamir’s Secret Sharing) thành nhiều Unseal Key.
- Khi bạn chạy
vault operator unseal <key>
, Vault dùng key đó để ghép dần đến đủ threshold, rồi giải mã Master Key trong RAM → Vault sang trạng thái unsealed.
Ví dụ:
vault operator init -key-shares=3 -key-threshold=2
- Sẽ sinh ra 3 Unseal Keys.
- Mỗi lần Vault khởi động, bạn phải nhập đủ 2 trong 3 keys để mở.
Mục tiêu bảo mật: Không ai một mình mở được Vault, tránh trường hợp admin xấu hoặc kẻ tấn công có toàn quyền. unseal là gì? bật audit để làm gì?
Unseal trong Vault là gì?
- Vault lưu trữ dữ liệu (secret, token, policy...) ở backend storage (VD: Raft, Consul) ở dạng mã hóa.
- Mỗi khi Vault khởi động lại (restart, crash, upgrade…), nó ở trạng thái sealed — tức là dữ liệu vẫn được mã hóa, chưa thể đọc/ghi.
- Để “mở khóa” dữ liệu, bạn phải unseal Vault bằng Unseal Key (có thể chia thành nhiều key, yêu cầu đủ
key-threshold
mới mở được).
Cơ chế bên trong:
- Khi init, Vault tạo một Master Key để giải mã dữ liệu.
- Master Key không được lưu thẳng, mà chia nhỏ (Shamir’s Secret Sharing) thành nhiều Unseal Key.
- Khi bạn chạy
vault operator unseal <key>
, Vault dùng key đó để ghép dần đến đủ threshold, rồi giải mã Master Key trong RAM → Vault sang trạng thái unsealed.
Ví dụ:
vault operator init -key-shares=3 -key-threshold=2
- Sẽ sinh ra 3 Unseal Keys.
- Mỗi lần Vault khởi động, bạn phải nhập đủ 2 trong 3 keys để mở.
Mục tiêu bảo mật: Không ai một mình mở được Vault, tránh trường hợp admin xấu hoặc kẻ tấn công có toàn quyền.
Tiếp theo, chúng ta sẽ xem cách mà deployment lấy giá trị trong Vault Secret như thế nào?
Repo : https://gitlab.com/kiettt164/k8s-lab.git
Thư mục : k8s-svc-lab
App của chúng ta có cấu trúc như sau :
- Code:
import random
import time
import os
import mysql.connector
from datetime import datetime, timedelta def connect_mysql(): return mysql.connector.connect( host=os.getenv("MYSQL_HOST"), user=os.getenv("MYSQL_USER"), password=os.getenv("MYSQL_PASSWORD"), database="marketing_db" ) def generate_random_date(start_year=1970, end_year=2005): start = datetime(start_year, 1, 1) end = datetime(end_year, 12, 31) delta = end - start random_days = random.randint(0, delta.days) return (start + timedelta(days=random_days)).date() def insert_fake_data(): conn = connect_mysql() cursor = conn.cursor() for _ in range(100): full_name = random.choice(["Alice", "Bob", "Charlie", "David", "Batman", "Superman", "Wolverine", "Cyclops", "Spiderman"]) dob = generate_random_date() phone = f"+84{random.randint(100000000, 999999999)}" email = f"{full_name.lower()}{random.randint(1,100)}@example.com" address = random.choice(["Hanoi", "Ho Chi Minh City", "Da Nang", "Hai Phong", "Can Tho", "Gotham", "Star City", "Metropolit"]) balance = round(random.uniform(500.0, 5000.0), 2) cursor.execute(""" INSERT INTO customers (full_name, dob, phone, email, address, account_balance) VALUES (%s, %s, %s, %s, %s, %s) """, (full_name, dob, phone, email, address, balance)) print("Inserted:", full_name, dob, phone, email, address, balance) conn.commit() cursor.close() conn.close() if __name__ == "__main__": while True: insert_fake_data() print("Done one batch. Sleeping for 30 minutes...\n") time.sleep(1800)
- Dockerfile:
# Author : Kevin TRan
FROM python:3.10-alpine WORKDIR /app COPY generate_fake_data_mysql.py . RUN pip install kafka-python && pip install --no-cache-dir mysql-connector-python CMD ["python", "generate_fake_data_mysql.py"]
- Deployment:
apiVersion: apps/v1
kind: Deployment
metadata: name: generate-fake-data namespace: demo
spec: replicas: 1 selector: matchLabels: app: generate-fake-data template: metadata: labels: app: generate-fake-data spec: containers: - name: generate-fake-data image: registry.gitlab.com/kiettt164/k8s-lab/app-gen-data:v1 imagePullPolicy: IfNotPresent resources: limits: memory: "128Mi" cpu: "100m" requests: memory: "64Mi" cpu: "50m" env: - name: MYSQL_HOST valueFrom: configMapKeyRef: name: db-config key: host - name: MYSQL_USER valueFrom: secretKeyRef: name: db-secret key: mysql-user - name: MYSQL_PASSWORD valueFrom: secretKeyRef: name: db-secret key: mysql-password ---
apiVersion: v1
kind: Service
metadata: name: generate-fake-data namespace: etl-lab-dev
spec: selector: app: generate-fake-data type: ClusterIP ports: - name: generate-fake-data protocol: TCP port: 5000 targetPort: 5000
Theo cách truyền thống, ta sẽ tạo configmap và secret như sau :
- Configmap:
apiVersion: v1
kind: ConfigMap
metadata: name: db-config namespace: demo
data: host: databases-server
- Secret:
apiVersion: v1
kind: Secret
metadata: name: db-secret namespace: demo
type: Opaque
data: mysql-root-password: VGVjaEAxNjA0 mysql-user: ZXRsX3VzZXI= mysql-password: MTIzNDU2
Nhưng bây giờ, đã có Vault rồi, chúng ta cần map lại secret qua Vault.
Trên master node, tạo thư mục rbac-vaults
mkdir rbac-vaults && cd rbac-vaults
- Tạo SA & RBAC tối thiểu trong namespace
demo
vim demo-app-sa.yaml ---
apiVersion: v1
kind: ServiceAccount
metadata: name: demo-app namespace: demo
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: name: secret-manager namespace: demo
rules: - apiGroups: [""] resources: ["secrets"] verbs: ["get","list","watch","create","update","patch","delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: name: secret-manager-rb namespace: demo
subjects: - kind: ServiceAccount name: demo-app namespace: demo
roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: secret-manager ---
kubectl apply -f demo-app-sa.yaml
- Dùng ExternalSecret để sync lại thành K8s Secret
db-secret
Cài ESO (tự cài CRDs)
kubectl create ns external-secrets helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets external-secrets/external-secrets -n external-secrets
- Khai báo SecretStore (ESO) trỏ tới Vault
vim secrets-store.yaml
---
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata: name: vault-demo namespace: demo
spec: provider: vault: server: "http://10.100.1.104:8200" path: "demo-db-secrets" version: "v2" auth: kubernetes: mountPath: "kubernetes" role: "demo-app" serviceAccountRef: name: demo-app --- kubectl apply -f secrets-store.yaml
#Xac nhan CRDs kubectl get crd | grep external-secrets # Bat buoc phai co:
# externalsecrets.external-secrets.io
# secretstores.external-secrets.io
# clustersecretstores.external-secrets.io
# clusterexternalsecrets.external-secrets.io
# pushsecrets.external-secrets.io (tùy bản)
Tạo ExternalSecret để sync về Secret K8s
vim externalsecret-db-secret.yaml --- apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: name: db-secret namespace: demo
spec: refreshInterval: 1m secretStoreRef: name: vault-demo kind: SecretStore target: name: db-secret creationPolicy: Owner template: type: Opaque data: - secretKey: mysql-user remoteRef: key: "demo/db-secret" property: "mysql-user" - secretKey: mysql-password remoteRef: key: "demo/db-secret" property: "mysql-password" - secretKey: mysql-root-password remoteRef: key: "demo/db-secret" property: "mysql-root-password" ---
kubectl apply -f externalsecret-db-secret.yaml
Bây giờ, deploy app và check xem DB có Data k nhé
kubectl apply -f gen-data-app.yaml
Check logs:
Check data trong mysql server :
HEHE!