Triển khai External Secrets Operator trên Kubernetes: Đồng bộ Vault và Cloud Secret Manager

Security tutorial - IT technology blog
Security tutorial - IT technology blog

Vấn đề thực tế: Tràn lan Secret và bài toán GitOps khó nhằn

Trong một đợt diễn tập xử lý sự cố trên cụm staging năm ngoái, nhóm chúng tôi đã phát hiện một lỗ hổng nghiêm trọng. Một lập trình viên đã vô tình đẩy manifest Kubernetes Secret chưa mã hóa lên một mirror công khai trên GitHub. File đó chứa thông tin xác thực cho cơ sở dữ liệu PostgreSQL môi trường staging. Dữ liệu trông có vẻ xáo trộn, nhưng Base64 thực chất chỉ là một chuẩn mã hóa ký tự (encoding scheme). Nó hoàn toàn không mang tính bảo mật mã hóa (encryption), và bất kỳ ai có quyền truy cập shell đều giải mã được chỉ trong vỏn vẹn hai giây.

Khi các cụm phát triển vượt quá hàng chục microservice, việc quản lý thông tin xác thực nhanh chóng trở nên hỗn loạn:

  • Rào cản với GitOps: Các công cụ như ArgoCD và Flux hoạt động hiệu quả nhất khi mọi thay đổi trạng thái đều được lưu trữ trên Git. Tuy nhiên, việc đẩy các manifest Kubernetes Secret dạng thô sẽ làm lộ mật khẩu nhạy cảm cho bất kỳ ai có quyền đọc repository.
  • Nỗi ám ảnh xoay vòng khóa (rotation): Việc xoay vòng mật khẩu master của RDS hay Stripe API key không nên chiếm trọn cả buổi sáng làm việc. Việc vá thủ công ba mươi secret phân tán theo namespace vừa tốn thời gian vừa thường xuyên làm gián đoạn các kết nối đang hoạt động.
  • Điểm mù trong nhật ký kiểm toán (audit log): HashiCorp Vault và AWS Secrets Manager ghi lại nhật ký cho từng yêu cầu đọc. Tuy nhiên, một khi secret đã được đưa vào Kubernetes etcd, các hệ thống audit log bên ngoài này sẽ không thể theo dõi pod nào đang đọc dữ liệu đó nữa.

Trước khi đẩy bất kỳ thông tin xác thực gốc mới nào vào kho lưu trữ trung tâm, hãy tạo các chuỗi ký tự có độ ngẫu nhiên cao (entropy). Chúng tôi thường sử dụng công cụ chạy trực tiếp trên trình duyệt tại toolcraft.app/vi/tools/security/password-generator để tạo mật khẩu máy chủ. Vì công cụ này xử lý hoàn toàn phía client, các token nhạy cảm sẽ không bao giờ bị truyền qua mạng. Một khi đã có thông tin xác thực mạnh, thách thức tiếp theo là đưa chúng vào các pod một cách gọn gàng và an toàn.

Nguyên nhân gốc rễ: Vì sao Kubernetes Secrets mặc định là chưa đủ

Để hiểu rõ lý do vì sao mô hình này bộc lộ nhiều điểm yếu dưới tải production thực tế, hãy xem cách Kubernetes lưu trữ secret nội bộ.

1. Base64 hoàn toàn không đảm bảo tính bảo mật

Các manifest của Kubernetes lưu trữ secret trong trường data dưới dạng chuỗi Base64. Chỉ cần chạy echo "<string>" | base64 -d, toàn bộ token production của bạn sẽ hiện rõ dưới dạng văn bản thuần túy ngay trên màn hình terminal. Nếu bạn cần kiểm tra các token thô trong quá trình gỡ lỗi cục bộ, hãy sử dụng công cụ Base64 Encoder/Decoder trên ToolCraft. Nhưng hãy luôn đảm bảo rằng các payload đã giải mã đó không bao giờ bị commit vào hệ thống quản lý phiên bản.

2. Sự lệch pha về vòng đời dữ liệu

Các hệ thống quản lý bên ngoài như HashiCorp Vault, AWS Secrets Manager và GCP Secret Manager hỗ trợ tự động xoay vòng khóa, quản lý thời hạn cấp phép (lease) và kiểm soát ranh giới IAM nghiêm ngặt. Kubernetes hoàn toàn không nắm bắt được những thay đổi đó. Khi mật khẩu cơ sở dữ liệu bên ngoài được xoay vòng lúc 02:00 sáng, etcd vẫn tiếp tục phân phối giá trị cũ cho đến khi pipeline CI hoặc một kỹ sư can thiệp để đẩy bản cập nhật mới.

3. Quyền đọc trên cụm quá rộng

Các kỹ sư thường được cấp quyền chỉ đọc (read-only) trên các namespace môi trường non-production. Việc thực thi lệnh kubectl get secret -o yaml sẽ ngay lập tức làm lộ mọi thông tin xác thực trong namespace đó, trừ khi cụm của bạn được bảo vệ bằng cơ chế mã hóa phong bì KMS (envelope encryption) chuyên dụng và các chính sách phân quyền RBAC chặt chẽ.

Đánh giá các giải pháp

Trong bốn năm qua, đội ngũ hạ tầng của chúng tôi đã đánh giá bốn mô hình phổ biến để kết nối các vault bên ngoài với các pod Kubernetes.

Phương pháp Cơ chế Ưu điểm Nhược điểm
Sealed Secrets (Bitnami) Mã hóa bất đối xứng thông qua khóa controller nội bộ trong cụm. Tương thích tốt với GitOps; manifest đã mã hóa có thể lưu trữ an toàn trong các repository công khai. Không tích hợp với enterprise vault; việc xoay vòng mật khẩu đòi hỏi phải mã hóa lại thủ công.
Vault Agent Sidecar Mutating webhook tự động tiêm một sidecar container vào từng pod để render secret vào volume bộ nhớ chia sẻ. Quản lý dynamic lease trực tiếp; không bao giờ tạo Kubernetes Secret gốc. Tốn thêm 40–80 MB RAM cho mỗi pod; ứng dụng phải đọc từ file path thay vì biến môi trường tiêu chuẩn.
Secrets Store CSI Driver Mount secret từ nhà cung cấp bên ngoài trực tiếp thành volume cục bộ của container. Dự án chính thức thuộc Kubernetes SIG; hỗ trợ rộng rãi các cloud provider. Việc đồng bộ sang đối tượng Secret gốc đòi hỏi thêm CRD và flag cấu hình; pod bắt buộc phải mount volume thì quá trình đồng bộ mới được kích hoạt.
External Secrets Operator (ESO) Một controller duy nhất đồng bộ API bên ngoài thành các đối tượng Kubernetes Secret tiêu chuẩn. Hỗ trợ linh hoạt Vault, AWS, GCP và Azure; tương thích tốt với biến môi trường tiêu chuẩn; tích hợp sẵn khả năng phát hiện sai lệch dữ liệu (drift detection). Vẫn để lại các đối tượng Kubernetes Secret tiêu chuẩn bên trong namespace.

Hướng dẫn từng bước: Triển khai External Secrets Operator (ESO)

Đối với phần lớn các đội ngũ kỹ thuật đang vận hành pipeline GitOps, External Secrets Operator (ESO) mang lại sự cân bằng tối ưu nhất. Ứng dụng của bạn vẫn tiếp tục đọc dữ liệu từ các biến môi trường tiêu chuẩn, trong khi HashiCorp Vault hoặc Cloud Secret Manager vẫn đóng vai trò là nguồn dữ liệu gốc đáng tin cậy duy nhất (single source of truth).

Bước 1: Cài đặt ESO bằng Helm

Đầu tiên, kéo Helm chart chính thức về và cài đặt operator vào namespace riêng biệt của nó:

helm repo add external-secrets https://charts.external-secrets.io
helm repo update

helm install external-secrets \
  external-secrets/external-secrets \
  -n external-secrets \
  --create-namespace \
  --set installCRDs=true

Kiểm tra đảm bảo cả ba pod cốt lõi—controller, webhook và cert-controller—đều đang hoạt động ổn định:

kubectl get pods -n external-secrets

Bước 2: Cấu hình xác thực với kho lưu trữ upstream

ESO cung cấp hai custom resource để kết nối upstream: SecretStore dành cho từng namespace đơn lẻ và ClusterSecretStore dùng cho backend trên toàn bộ cụm. Dưới đây là cấu hình đã được kiểm chứng trên production để kết nối với HashiCorp Vault thông qua một Kubernetes ServiceAccount chuyên dụng:

# Tạo service account chuyên dụng để xác thực với Vault
kubectl create serviceaccount vault-auth-sa -n production

Lưu manifest này dưới tên vault-secretstore.yaml:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: production
spec:
  provider:
    vault:
      server: "https://vault.internal.mycompany.com:8200"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "production-app-role"
          serviceAccountRef:
            name: vault-auth-sa

Áp dụng cấu hình:

kubectl apply -f vault-secretstore.yaml

Nếu bạn triển khai trên AWS EKS, hãy trỏ ESO tới AWS Secrets Manager bằng IAM Roles for Service Accounts (IRSA):

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-backend
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: aws-irsa-sa

Bước 3: Định nghĩa Custom Resource ExternalSecret

Sau khi thiết lập xác thực thành công, hãy định nghĩa cấu hình ánh xạ giữa các key bên ngoài và Kubernetes secret gốc. Nếu secret bên ngoài của bạn chứa cấu trúc JSON lồng nhau, hãy kiểm tra kỹ cấu trúc payload trước bằng công cụ JSON Formatter & Validator trên ToolCraft. Chỉ một dấu ngoặc sai cú pháp trong Vault cũng có thể làm gián đoạn quá trình đồng bộ (reconciliation) của operator trên toàn bộ namespace.

Tạo file app-externalsecret.yaml:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: payment-service-secret-sync
  namespace: production
spec:
  refreshInterval: "1h"
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: payment-service-db-credentials
    creationPolicy: Owner
  data:
    - secretKey: DB_PASSWORD
      remoteRef:
        key: production/payment-service
        property: password
    - secretKey: DB_USER
      remoteRef:
        key: production/payment-service
        property: username

Áp dụng manifest:

kubectl apply -f app-externalsecret.yaml

Bước 4: Xác nhận trạng thái đồng bộ

Kiểm tra cờ trạng thái trên custom resource:

kubectl get externalsecret payment-service-secret-sync -n production

Xác nhận rằng cột STATUS hiển thị SecretSynced:

NAME                          STORE           REFRESH INTERVAL   STATUS         READY
payment-service-secret-sync   vault-backend   1h                 SecretSynced   True

Bây giờ hãy kiểm tra Kubernetes Secret vừa được tạo ra:

kubectl get secret payment-service-db-credentials -n production -o yaml

Bước 5: Gắn Secret vào Deployment

Các pod ứng dụng của bạn không cần bất kỳ SDK client tùy chỉnh hay sidecar nào. Bạn chỉ cần nạp các key đã đồng bộ thông qua các biến môi trường tiêu chuẩn:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-service
  namespace: production
spec:
  replicas: 2
  selector:
    matchLabels:
      app: payment-service
  template:
    metadata:
      labels:
        app: payment-service
    spec:
      containers:
        - name: app
          image: payment-service:v1.2.0
          env:
            - name: DATABASE_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: payment-service-db-credentials
                  key: DB_PASSWORD
            - name: DATABASE_USER
              valueFrom:
                secretKeyRef:
                  name: payment-service-db-credentials
                  key: DB_USER

Best Practice khi triển khai trên Production

Dưới đây là một số bài học kinh nghiệm quý giá từ quá trình quản lý ESO trên các cụm production đa người dùng (multi-tenant):

  • Kiểm soát chi phí gọi API: Tránh đặt refreshInterval với khoảng thời gian quá ngắn như 5s. AWS Secrets Manager tính phí $0.05 cho mỗi 10.000 API request. Việc thăm dò liên tục 50 secret mỗi năm giây sẽ tạo ra hơn 25 triệu lượt gọi mỗi tháng, phát sinh thêm $130 chi phí API ngoài dự kiến. Khoảng thời gian từ 1h đến 4h là lựa chọn hợp lý cho thông tin xác thực tĩnh của ứng dụng.
  • Tự động khởi động lại pod: Kubernetes không tự động restart pod khi Secret bên dưới thay đổi. Hãy triển khai Stakater Reloader song song với ESO. Việc thêm annotation reloader.stakater.com/auto: "true" vào Deployment sẽ kích hoạt rolling restart mượt mà ngay khi ESO cập nhật secret.
  • Giữ Git luôn sạch: Chỉ commit các khai báo ExternalSecret và SecretStore vào Git. Toàn bộ thông tin xác thực nhạy cảm vẫn được bảo vệ an toàn bên trong kho lưu trữ khóa trung tâm.
Share: