+2

Kubernetes : Intergrate Vault secret for K8s Cluster.

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ý:

image.png image.png image.png

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 :

  1. Vẫn là 1 app gen random data vào Mysql.
  2. Trong code, có chỉ định sẽ dùng env để chỉ định là sử dụng host, mysql password…
  3. 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 :

  1. 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
  1. 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
  1. 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

image.png

image.png

  1. 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
  1. Tạo KV v2
vault secrets enable -path=kv kv-v2
  1. 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 reviewerCA 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

  1. 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

image.png

image.png

  1. 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

image.png

  1. 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.

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 :

  1. 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)

  1. 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"]
  1. 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 :

  1. Configmap:
apiVersion: v1
kind: ConfigMap
metadata:
  name: db-config
  namespace: demo
data:
  host: databases-server
  1. 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
  1. 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

  1. 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
  1. 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:

image.png

Check data trong mysql server :

image.png

HEHE!

Dưới đây là 1 số những video của mình về K8s topic

  1. Docker to K8s : https://www.youtube.com/watch?v=bPFfetEj1qs&t=1504s
  2. K8s inbound agent : https://www.youtube.com/watch?v=7Ll_Xx0RM00&t=1s
  3. MetalLB : https://www.youtube.com/watch?v=KvuI8u3BIZs&t=1s

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí