cloud

在 Kubernetes 使用 Vault 與 External Secrets

不把 secret 值提交到 Git,將 Vault KV 資料同步為 Kubernetes Secret。

English繁中
在 Kubernetes 使用 Vault 與 External Secrets

我的家用 Kubernetes 叢集以 Vault 保存 secret 值,External Secrets Operator(ESO)負責讀取並建立一般的 Kubernetes Secrets,Deployment 再照常引用。Git 保存 manifests 與來源對應關係,不保存明文值。

Vault KV v2 → ClusterSecretStore → ExternalSecret → Kubernetes Secret → Pod

這樣重建叢集時,就不必逐一手動複製 Secrets。以下以 example-apiexample-worker 與內部使用的 example-admin 示範。

由 Argo CD 安裝 External Secrets Operator

我的 GitOps repository 透過 Helm chart 安裝 ESO:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: external-secrets
  namespace: argocd
  annotations:
    argocd.argoproj.io/sync-wave: "-10"
spec:
  project: default
  source:
    repoURL: https://charts.external-secrets.io
    chart: external-secrets
    targetRevision: 2.5.0
    helm:
      releaseName: external-secrets
      valuesObject:
        installCRDs: true
  destination:
    server: https://kubernetes.default.svc
    namespace: external-secrets
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
      - ServerSideApply=true

ESO 與 CRDs 要先於 ClusterSecretStore、ExternalSecret 建立。Sync wave 表達這項順序,套用依賴資源前仍要確認 controller 就緒。

將設定檔存入 Vault KV v2

Vault 的 KV v2 mount 名為 secret

Vault KV v2 secret engine

三個應用程式分別保存 dotenv、JSON 與 YAML 設定:

vault kv put secret/example-api/env-file dotenv=@.env
vault kv put secret/example-worker/config-file config.json=@config.json
vault kv put secret/example-admin/config-file config=@config.yml

API 的對應關係是 Vault path secret/example-api/env-file、property dotenv,產生 Kubernetes Secret example-api-env-file 裡的 .env key。Path 與 property 都必須和後面的 ExternalSecret 一致。

建立 Vault Kubernetes auth

ESO 使用 external-secrets namespace 裡同名的 ServiceAccount 向 Vault 驗證。Vault 則需要 Kubernetes API 的 TokenReview 能力,查驗送來的 JWT。

為 reviewer 建立專用 ServiceAccount,並綁定 system:auth-delegator。這個角色允許委派驗證與授權查詢,包含 TokenReview:

kubectl create namespace vault-auth --dry-run=client -o yaml | kubectl apply -f -
kubectl -n vault-auth create serviceaccount vault-auth
kubectl create clusterrolebinding vault-auth-tokenreview \
  --clusterrole=system:auth-delegator \
  --serviceaccount=vault-auth:vault-auth

將以下內容存成 vault-auth-token.yaml

apiVersion: v1
kind: Secret
metadata:
  name: vault-auth-token
  namespace: vault-auth
  annotations:
    kubernetes.io/service-account.name: vault-auth
type: kubernetes.io/service-account-token

建立 Secret 後,先等待 token controller 填入資料;若等待失敗,先停止後續步驟。確認 kubeconfig context 指向目標 cluster,再取得 reviewer JWT 與 CA;--flatten 也會納入以檔案路徑引用的 CA:

kubectl apply -f vault-auth-token.yaml
kubectl -n vault-auth wait --for=jsonpath='{.data.token}' secret/vault-auth-token --timeout=60s
TOKEN_REVIEWER_JWT=$(kubectl -n vault-auth get secret vault-auth-token -o jsonpath='{.data.token}' | base64 -d)
kubectl config view --raw --minify --flatten -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d > ca.crt

由有權管理 Vault 的身分設定 Kubernetes auth:

export VAULT_ADDR=https://vault.example.internal:8200
vault login

vault auth enable kubernetes

vault write auth/kubernetes/config \
  kubernetes_host="https://rke-api.example.internal:6443" \
  kubernetes_ca_cert=@ca.crt \
  token_reviewer_jwt="$TOKEN_REVIEWER_JWT"

如果 auth mount 已存在,保留它,只更新 auth/kubernetes/config 的 API server、CA 與 reviewer JWT。

Vault 1.9 起,新建 Kubernetes auth mount 預設使用 disable_iss_validation=true,由 Kubernetes 在 TokenReview 時驗證 issuer。舊 mount 可能保留 Vault 端的 issuer 驗證;遇到 mismatch 時應先檢查現有設定,不能把「沒有寫這個選項」解讀成已啟用 Vault 端驗證。這也與 TLS 憑證驗證是不同的事。參考 Vault Kubernetes auth 文件

Reviewer token 不進 Git;叢集或 Vault auth 配置重建時,也需要處理其輪替。

限制可讀取的 KV 路徑

每個應用程式使用自己的 policy 與 role。API 的設定如下:

vault policy write example-api - <<'EOF'
path "secret/data/example-api/*" {
  capabilities = ["read"]
}
EOF

vault write auth/kubernetes/role/external-secrets-example-api \
  bound_service_account_names=external-secrets \
  bound_service_account_namespaces=external-secrets \
  audience=https://kubernetes.default.svc.cluster.local \
  policies=example-api \
  ttl=1h

Worker 與 admin 使用各自的路徑:

vault policy write example-worker - <<'EOF'
path "secret/data/example-worker/*" {
  capabilities = ["read"]
}
EOF

vault write auth/kubernetes/role/external-secrets-example-worker \
  bound_service_account_names=external-secrets \
  bound_service_account_namespaces=external-secrets \
  audience=https://kubernetes.default.svc.cluster.local \
  policies=example-worker \
  ttl=1h
vault policy write example-admin - <<'EOF'
path "secret/data/example-admin/*" {
  capabilities = ["read"]
}
EOF

vault write auth/kubernetes/role/external-secrets-example-admin \
  bound_service_account_names=external-secrets \
  bound_service_account_namespaces=external-secrets \
  audience=https://kubernetes.default.svc.cluster.local \
  policies=example-admin \
  ttl=1h

Policy 使用 KV v2 的 API path secret/data/...;ExternalSecret 的 remote key 則相對於 mount,不含 secret/data/

Role 的 audience 要符合 ESO 要求的 JWT。下方 Store 的 serviceAccountRef.audiences 明確使用相同值,部署時仍須核對 cluster 設定。參考 ESO Kubernetes authentication

讓 ClusterSecretStore 信任 Vault

先將簽發 Vault server certificate 的公開 CA 憑證存入 ConfigMap:

kubectl -n external-secrets create configmap vault-ca \
  --from-file=ca.crt=./vault-ca.crt \
  --dry-run=client -o yaml | kubectl apply -f -

Store 宣告 Vault URL、KV mount、auth role 與 CA 來源:

apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
  name: vault-example-api
spec:
  provider:
    vault:
      server: "https://vault.example.internal:8200"
      path: "secret"
      version: "v2"
      caProvider:
        type: ConfigMap
        name: vault-ca
        namespace: external-secrets
        key: ca.crt
      auth:
        kubernetes:
          mountPath: kubernetes
          role: external-secrets-example-api
          serviceAccountRef:
            name: external-secrets
            namespace: external-secrets
            audiences:
              - https://kubernetes.default.svc.cluster.local

Kubernetes auth token 與 secret 值會經過這條連線,因此 Vault 使用 HTTPS,Store 以 caProvidercaBundle 驗證憑證。若 URL 使用 vault.example.internal,server certificate 的 SAN 必須包含該名稱;直接使用 IP 時則需要對應的 IP SAN。

這裡有兩種 CA:Vault auth 設定中的 cluster CA 用於連線 Kubernetes API;Store 的 Vault CA 用於連線 Vault。兩者不能混用。

建立 ExternalSecret

Store 就緒後,在應用程式 namespace 建立:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: example-api-env-file
  namespace: example-api
spec:
  refreshInterval: 2m
  secretStoreRef:
    name: vault-example-api
    kind: ClusterSecretStore
  target:
    name: example-api-env-file
    creationPolicy: Owner
  data:
    - secretKey: .env
      remoteRef:
        key: example-api/env-file
        property: dotenv

ESO 依照 refreshInterval: 2m 定期讀取 secret/example-api/env-filedotenv property,將內容放進 example-api-env-file Secret 的 .env key。Deployment 再掛載或以應用程式支援的方式讀取它。

驗證 controller、Store 與 Secret

先檢查 ESO 和 CRDs:

kubectl -n external-secrets get pods
kubectl get crd | grep external-secrets

再看 Store 與產生的 Secret:

kubectl get clustersecretstore
kubectl describe clustersecretstore vault-example-api
kubectl -n example-api get secret example-api-env-file

Worker 與 admin 也要先依各自的 role、path 與 property 建立對應 Store 和 ExternalSecret,再檢查產生的 Secrets:

kubectl -n example-worker get secret example-worker-config-file
kubectl -n example-admin get secret example-admin-config-file

同時確認 ExternalSecret 的 Ready 狀態,以及 workload 是否已讀到預期設定。Store 不健康時,查看 ESO logs:

kubectl -n external-secrets logs deploy/external-secrets --tail=120

常見原因包括 Vault auth 指向舊 API server、cluster CA 錯誤、reviewer JWT 過期或來自其他 cluster、role 綁錯 ServiceAccount/namespace、KV v2 policy path 少了 data/,或 Vault 中沒有預期的 property。

何時改用 SOPS

這套配置以 Vault 為 source of truth,ESO 同步 runtime 值,Git 只保存引用。若希望 Git 裡的加密檔成為 source of truth,或需要不依賴 Vault 的離線 bootstrap,才考慮 SOPS。對目前的流程而言,另外加入 SOPS 也代表多一套需要管理的金鑰與交付方式。