DevOps GitOps密鑰管理:從Sealed Secrets到External Secrets的6種生產模式

DevOps

把Secret提交到Git?你在埋定時炸彈

GitOps的核心是「Git即唯一可信源」,但Kubernetes Secret預設只是Base64編碼——不是加密。把Base64編碼的Secret推到Git儲存庫,等於把資料庫密碼、API Key、TLS憑證明文放在公開場合。一旦儲存庫洩露(內部人員誤操作、第三方供應鏈被攻破、fork的儲存庫忘記清理),所有密鑰瞬間暴露。

2026年,DevOps GitOps密鑰管理已經不是「可選項」,而是生產上線的硬性要求。本文覆蓋6種生產級密鑰管理模式,從Sealed Secrets加密儲存到External Secrets Operator動態注入,幫你徹底解決GitOps場景下的密鑰安全難題。

核心收穫

  • 理解GitOps密鑰管理的3種架構模式:加密儲存、外部注入、混合模式
  • 掌握Sealed Secrets、External Secrets Operator、SOPS的完整部署與配置
  • 實現密鑰自動輪換和多叢集同步
  • 避開5個最常見的生產坑和10個高頻報錯
  • 獲得對比選型決策矩陣

目錄

  • GitOps密鑰管理核心概念
  • Pattern 1: Sealed Secrets加密儲存
  • Pattern 2: External Secrets Operator + Vault
  • Pattern 3: SOPS + Age/GPG加密
  • Pattern 4: 密鑰自動輪換
  • Pattern 5: 多叢集密鑰同步
  • Pattern 6: 稽核與合規
  • 5個常見坑及解決方案
  • 10個常見報錯排查
  • 進階優化技巧
  • 對比分析
  • 線上工具推薦

GitOps密鑰管理核心概念

三種架構模式

┌─────────────────────────────────────────────────────────────┐
│                 GitOps密鑰管理三種架構模式                      │
├─────────────────┬──────────────────┬────────────────────────┤
│  加密儲存模式     │  外部注入模式      │  混合模式              │
│  (Sealed Secrets)│  (ESO + Vault)   │  (SOPS + ESO)         │
├─────────────────┼──────────────────┼────────────────────────┤
│  Git存加密密文    │  Git只存引用      │  Git存加密密文+引用     │
│  叢集內解密       │  叢集內拉取明文    │  叢集內解密或拉取       │
│  離線可稽核       │  依賴外部服務      │  彈性組合              │
│  輪換需重新加密   │  天然支援輪換      │  部分支援輪換          │
└─────────────────┴──────────────────┴────────────────────────┘

K8s Secret的本質問題

# 建立一個普通Secret
kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password='SuperSecret123!'

# 「加密」只是Base64編碼,任何人都能解碼
kubectl get secret db-credentials -o yaml

# 解碼只需要一行命令
kubectl get secret db-credentials \
  -o jsonpath='{.data.password}' | base64 -d
# 輸出: SuperSecret123!

# 使用工具庫Base64編解碼驗證
# https://toolsku.com/zh-TW/encode/base64

DevOps GitOps密鑰管理的核心原則

原則 說明 違反後果
零明文儲存 Git儲存庫中絕不出現明文密鑰 儲存庫洩露即全面失守
最小權限 每個應用只存取自己需要的密鑰 密鑰橫向洩露
自動輪換 密鑰定期自動更新 長期密鑰被暴力破解
可稽核 每次密鑰存取都有記錄 無法追溯洩露源頭
可恢復 密鑰遺失後可快速恢復 業務中斷

Pattern 1: Sealed Secrets加密儲存

Sealed Secrets是Bitnami開源的GitOps密鑰管理方案。核心思路:在本機用kubeseal加密Secret,生成SealedSecret資源提交到Git,叢集內的Sealed Secrets Controller自動解密還原為K8s Secret。

架構圖

┌──────────────────────────────────────────────────────┐
│                   開發者工作站                          │
│  ┌────────────┐    ┌────────────┐                     │
│  │ secret.yaml │───▶│  kubeseal   │                    │
│  │ (明文,不提交)│    │  (加密工具)  │                    │
│  └────────────┘    └─────┬──────┘                     │
│                          │ 加密                        │
│                    ┌─────▼──────┐                     │
│                    │sealed-secret│                     │
│                    │  .yaml(密文)│                     │
│                    └─────┬──────┘                     │
└──────────────────────────┼───────────────────────────┘
                           │ git push
┌──────────────────────────▼───────────────────────────┐
│                    Git Repository                      │
│              (只儲存加密後的SealedSecret)               │
└──────────────────────────┬───────────────────────────┘
                           │ git pull (ArgoCD/Flux)
┌──────────────────────────▼───────────────────────────┐
│                 K8s Cluster                            │
│  ┌──────────────────────────────┐                     │
│  │  Sealed Secrets Controller    │                     │
│  │  (用私鑰解密SealedSecret)      │                     │
│  └──────────────┬───────────────┘                     │
│                 │ 解密                                 │
│           ┌─────▼──────┐                              │
│           │  K8s Secret │                              │
│           │  (明文,叢集內)│                              │
│           └────────────┘                               │
└───────────────────────────────────────────────────────┘

安裝Sealed Secrets

# 安裝Controller到叢集
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.0/controller.yaml

# 等待Controller就緒
kubectl wait --for=condition=available --timeout=120s \
  deployment/sealed-secrets-controller -n kube-system

# 安裝kubeseal CLI
KUBESEAL_VERSION=0.27.0
curl -sLO "https://github.com/bitnami-labs/sealed-secrets/releases/download/v${KUBESEAL_VERSION}/kubeseal-linux-amd64"
chmod +x kubeseal-linux-amd64
sudo mv kubeseal-linux-amd64 /usr/local/bin/kubeseal

# 驗證安裝
kubeseal --version

加密Secret

# 從明文Secret檔案加密
kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password='SuperSecret123!' \
  --dry-run=client -o yaml | \
  kubeseal --format yaml > sealed-secret.yaml

# 從檔案建立
kubectl create secret generic tls-cert \
  --from-file=tls.crt=server.crt \
  --from-file=tls.key=server.key \
  --dry-run=client -o yaml | \
  kubeseal --format yaml > sealed-tls.yaml

# 指定命名空間和名稱(嚴格模式)
kubectl create secret generic api-key \
  --from-literal=key=abc123xyz \
  --namespace production \
  --dry-run=client -o yaml | \
  kubeseal --format yaml \
  --scope namespace-wide > sealed-api-key.yaml

SealedSecret資源範例

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: db-credentials
  namespace: production
spec:
  encryptedData:
    username: AgBfj8k2mN3pQ7sT9vWxYz...
    password: AgCdH5lM6nO8qR0tU2wXyZa...
  template:
    metadata:
      name: db-credentials
      namespace: production
    type: Opaque

加密範圍控制

# strict: 只能在相同命名空間和名稱下解密(預設)
kubeseal --scope strict

# namespace-wide: 相同命名空間下可重新命名
kubeseal --scope namespace-wide

# cluster-wide: 叢集內任意命名空間可用
kubeseal --scope cluster-wide
範圍 安全等級 彈性 適用場景
strict 最高 最低 生產環境密鑰
namespace-wide 中 中 同命名空間多應用
cluster-wide 最低 最高 憑證等共享資源

備份與恢復主密鑰

# 備份主密鑰(遺失後所有SealedSecret無法解密!)
kubectl get secret -n kube-system \
  sealed-secrets-key -o yaml > sealed-secrets-key-backup.yaml

# 恢復主密鑰到新叢集
kubectl apply -f sealed-secrets-key-backup.yaml

# 輪換主密鑰
kubectl delete secret -n kube-system sealed-secrets-key
# Controller會自動生成新密鑰,舊SealedSecret仍可用

Pattern 2: External Secrets Operator + Vault

External Secrets Operator(ESO)將外部密鑰管理系統(Vault、AWS Secrets Manager、GCP Secret Manager等)與Kubernetes整合。Git儲存庫只儲存SecretStore引用,叢集內的ESO Controller從外部系統拉取密鑰並建立K8s Secret。

架構圖

┌──────────────────────────────────────────────────────┐
│                    Git Repository                      │
│  ┌──────────────┐  ┌──────────────┐                  │
│  │ SecretStore   │  │ ExternalSecret│                  │
│  │ (Vault連線配置)│  │ (密鑰引用映射) │                  │
│  └──────────────┘  └──────────────┘                  │
└──────────────────────┬───────────────────────────────┘
                       │ git pull
┌──────────────────────▼───────────────────────────────┐
│                 K8s Cluster                            │
│  ┌──────────────────────────────┐                     │
│  │   External Secrets Operator   │                     │
│  │   (監聽ExternalSecret資源)     │                     │
│  └──────────────┬───────────────┘                     │
│                 │ 拉取密鑰                              │
│    ┌────────────▼────────────┐                        │
│    │                         │                        │
│    ▼                         ▼                        │
│ ┌──────────┐         ┌──────────┐                    │
│ │  K8s     │         │ HashiCorp│                    │
│ │  Secret  │         │  Vault   │                    │
│ └──────────┘         └──────────┘                    │
└───────────────────────────────────────────────────────┘

安裝ESO

# 使用Helm安裝
helm repo add external-secrets https://charts.external-secrets.io
helm repo update

helm install external-secrets \
  external-secrets/external-secrets \
  --namespace external-secrets \
  --create-namespace \
  --set installCRDs=true \
  --set replicaCount=2 \
  --set leaderElect=true

# 驗證
kubectl get pods -n external-secrets

配置HashiCorp Vault SecretStore

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

Vault策略配置

# Vault策略:限制ESO只能存取特定路徑
path "secret/data/production/*" {
  capabilities = ["read"]
}

path "secret/data/production/database/*" {
  capabilities = ["read", "list"]
}

# 禁止存取其他環境的密鑰
path "secret/data/staging/*" {
  capabilities = ["deny"]
}

path "secret/data/development/*" {
  capabilities = ["deny"]
}

ExternalSecret資源

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: production
spec:
  refreshInterval: 15m
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: db-credentials
    creationPolicy: Owner
    template:
      type: Opaque
      data:
        DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@db.internal:5432/mydb"
  data:
    - secretKey: username
      remoteRef:
        key: secret/data/production/database
        property: username
    - secretKey: password
      remoteRef:
        key: secret/data/production/database
        property: password

多密鑰源:ClusterSecretStore

apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: aws-secrets-manager
spec:
  provider:
    aws:
      service: SecretsManager
      region: us-east-1
      auth:
        jwt:
          serviceAccountRef:
            name: eso-aws-sa
            namespace: external-secrets
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: aws-api-keys
  namespace: production
spec:
  refreshInterval: 5m
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: aws-api-keys
  dataFrom:
    - extract:
        key: production/api-keys

Pattern 3: SOPS + Age/GPG加密

SOPS(Secrets OPerationS)是Mozilla開發的密鑰加密工具,支援AES、PGP、Age等多種加密後端。與Sealed Secrets不同,SOPS加密的是檔案本身,可以加密YAML/JSON/ENV等格式,且只加密值不加密鍵——這意味著你可以Git diff看到密鑰結構變化。

架構圖

┌──────────────────────────────────────────────────────┐
│                   開發者工作站                          │
│  ┌────────────┐    ┌────────────┐                     │
│  │ secret.yaml │───▶│    SOPS     │                    │
│  │ (明文,不提交)│    │ + Age/GPG   │                    │
│  └────────────┘    └─────┬──────┘                     │
│                          │ 加密                        │
│                    ┌─────▼──────┐                     │
│                    │secret.enc.  │                     │
│                    │ yaml(密文)  │                     │
│                    └─────┬──────┘                     │
└──────────────────────────┼───────────────────────────┘
                           │ git push
┌──────────────────────────▼───────────────────────────┐
│                    Git Repository                      │
│        (儲存加密檔案,鍵名可見,值已加密)                 │
└──────────────────────────┬───────────────────────────┘
                           │ git pull
┌──────────────────────────▼───────────────────────────┐
│                 CI/CD Pipeline                         │
│  ┌──────────────────────────────┐                     │
│  │ sops --decrypt + kubectl apply│                    │
│  │ 或 Flux Kustomization SOPS整合│                    │
│  └──────────────────────────────┘                     │
└───────────────────────────────────────────────────────┘

安裝SOPS和Age

# 安裝SOPS
curl -sLO https://github.com/getsops/sops/releases/download/v3.9.0/sops-v3.9.0.linux.amd64
chmod +x sops-v3.9.0.linux.amd64
sudo mv sops-v3.9.0.linux.amd64 /usr/local/bin/sops

# 安裝Age(推薦加密後端,比GPG更簡單)
curl -sLO https://github.com/FiloSottile/age/releases/download/v1.2.0/age-v1.2.0-linux-amd64.tar.gz
tar xzf age-v1.2.0-linux-amd64.tar.gz
sudo mv age/age /usr/local/bin/
sudo mv age/age-keygen /usr/local/bin/

# 生成Age密鑰對
age-keygen -o age.key
# 公鑰: age1abc123xyz...

# 驗證
sops --version
age --version

加密Secret檔案

# secret.yaml(加密前)
apiVersion: v1
kind: Secret
metadata:
  name: app-config
  namespace: production
type: Opaque
stringData:
  DB_HOST: "db.internal.example.com"
  DB_PASSWORD: "SuperSecret123!"
  API_KEY: "sk-proj-abc123xyz456"
  REDIS_URL: "redis://redis.internal:6379"
# 使用Age公鑰加密
sops --encrypt \
  --age age1abc123xyz456... \
  --encrypted-regex '^(DB_PASSWORD|API_KEY)$' \
  --in-place secret.yaml
# secret.yaml(加密後)
apiVersion: v1
kind: Secret
metadata:
  name: app-config
  namespace: production
type: Opaque
stringData:
  DB_HOST: "db.internal.example.com"
  DB_PASSWORD: ENC[AES256_GCM,data:Wk5kPQ==,tag:abc123==,type:str]
  API_KEY: ENC[AES256_GCM,data:Zm9vYmFy,tag:def456==,type:str]
  REDIS_URL: "redis://redis.internal:6379"
sops:
  kms: []
  gcp_kms: []
  azure_kv: []
  hc_vault: []
  age:
    - recipient: age1abc123xyz456...
      enc: |
        -----BEGIN AGE ENCRYPTED FILE-----
        YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA...
        -----END AGE ENCRYPTED FILE-----
  lastmodified: "2026-06-16T10:00:00Z"
  mac: ENC[AES256_GCM,data:abc==,tag:xyz==,type:str]
  pgp: []
  encrypted_regex: ^(DB_PASSWORD|API_KEY)$
  version: 3.9.0

Flux CD整合SOPS

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: app-secrets
  namespace: flux-system
spec:
  interval: 10m
  path: ./clusters/production
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system
  decryption:
    provider: sops
    secretRef:
      name: sops-age-key
# 建立Age私鑰Secret供Flux解密
kubectl create secret generic sops-age-key \
  --namespace flux-system \
  --from-file=age.agekey=age.key \
  --dry-run=client -o yaml | \
  kubeseal --format yaml > sealed-sops-key.yaml

多密鑰加密(多人協作)

# .sops.yaml配置檔案——專案級加密配置
cat > .sops.yaml << 'EOF'
creation_rules:
  - path_regex: ^clusters/production/.*\.yaml$
    key_groups:
      - age:
          - age1abc123xyz456  # 生產環境密鑰
          - age1def789uvw012  # SRE團隊密鑰
  - path_regex: ^clusters/staging/.*\.yaml$
    key_groups:
      - age:
          - age1ghi345rst678  # 測試環境密鑰
  - path_regex: ^clusters/.*\.yaml$
    key_groups:
      - age:
          - age1abc123xyz456  # 預設密鑰
EOF

Pattern 4: 密鑰自動輪換

密鑰輪換是DevOps GitOps密鑰管理中最容易被忽視的環節。靜態密鑰長期不變,一旦洩露攻擊窗口無限大。2026年的最佳實踐是:所有生產密鑰90天輪換一次,高敏感密鑰7天輪換。

輪換架構

┌──────────────────────────────────────────────────────┐
│                  密鑰自動輪換架構                       │
│                                                       │
│  ┌──────────┐   觸發輪換   ┌──────────────┐          │
│  │ CronJob   │────────────▶│ Vault Rotate  │          │
│  │ (定時任務) │             │ (生成新密鑰)   │          │
│  └──────────┘              └──────┬───────┘          │
│                                   │ 新密鑰             │
│                            ┌──────▼───────┐          │
│                            │ ExternalSecret│          │
│                            │ (自動刷新)     │          │
│                            └──────┬───────┘          │
│                                   │ 更新               │
│                            ┌──────▼───────┐          │
│                            │  K8s Secret   │          │
│                            │ (自動更新)     │          │
│                            └──────┬───────┘          │
│                                   │ 滾動重啟           │
│                            ┌──────▼───────┐          │
│                            │   Pods        │          │
│                            │ (讀取新密鑰)   │          │
│                            └──────────────┘          │
└──────────────────────────────────────────────────────┘

ESO自動輪換配置

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: rotating-api-key
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: api-key
    creationPolicy: Owner
    template:
      type: Opaque
      metadata:
        annotations:
          reflector.v1.k8s.emberstack.com/reflection-allowed: "true"
  data:
    - secretKey: api-key
      remoteRef:
        key: secret/data/production/api
        property: key

Vault動態密鑰

# Vault資料庫動態憑證配置
resource "vault_database_secret_backend_connection" "postgresql" {
  backend       = "database"
  name          = "postgresql-production"
  allowed_roles = ["app-readonly", "app-readwrite"]

  postgresql {
    connection_url = "postgresql://{{username}}:{{password}}@db.internal:5432/mydb?sslmode=require"
    username       = "vault_admin"
    password       = "VaultAdminPassword123!"
  }
}

resource "vault_database_secret_backend_role" "app_readwrite" {
  backend             = "database"
  name                = "app-readwrite"
  db_name             = vault_database_secret_backend_connection.postgresql.name
  default_ttl         = 3600
  max_ttl             = 86400
  creation_statements = [
    "CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
    "GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"
  ]
}

應用自動感知密鑰變化

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
      annotations:
        secret.reloader.stakater.com/reload: "api-key,db-credentials"
    spec:
      containers:
        - name: api-server
          image: registry.example.com/api-server:v2.1.0
          envFrom:
            - secretRef:
                name: api-key
            - secretRef:
                name: db-credentials
# 安裝Reloader——Secret變更時自動滾動重啟Pod
helm repo add stakater https://stakater.github.io/stakater-charts
helm repo update

helm install reloader stakater/reloader \
  --namespace reloader \
  --create-namespace \
  --set reloader.watchGlobally=false

Pattern 5: 多叢集密鑰同步

多叢集場景下,密鑰需要在叢集間安全同步。DevOps GitOps密鑰管理要求:每個叢集有獨立的密鑰生命週期,同時保持一致性。

多叢集架構

┌──────────────────────────────────────────────────────┐
│                  多叢集密鑰同步架構                     │
│                                                       │
│  ┌─────────────────────────────────────┐             │
│  │        HashiCorp Vault (中央)        │             │
│  │  ┌──────────┐  ┌──────────┐        │             │
│  │  │prod-east/│  │prod-west/│        │             │
│  │  │ secrets  │  │ secrets  │        │             │
│  │  └──────────┘  └──────────┘        │             │
│  └──────────┬───────────────┬──────────┘             │
│             │               │                         │
│    ┌────────▼──────┐ ┌──────▼────────┐              │
│    │  Cluster East  │ │  Cluster West │              │
│    │  ┌──────────┐ │ │ ┌──────────┐ │              │
│    │  │   ESO    │ │ │ │   ESO    │ │              │
│    │  │Controller│ │ │ │Controller│ │              │
│    │  └────┬─────┘ │ │ └────┬─────┘ │              │
│    │       ▼       │ │      ▼       │              │
│    │  ┌──────────┐ │ │ ┌──────────┐ │              │
│    │  │ Secrets  │ │ │ │ Secrets  │ │              │
│    │  └──────────┘ │ │ └──────────┘ │              │
│    └───────────────┘ └──────────────┘              │
└──────────────────────────────────────────────────────┘

每個叢集獨立SecretStore

# Cluster East
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-east
  namespace: production
spec:
  provider:
    vault:
      server: "https://vault.internal.example.com"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes/east"
          role: "eso-east-role"
          serviceAccountRef:
            name: "external-secrets-sa"
# Cluster West
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-west
  namespace: production
spec:
  provider:
    vault:
      server: "https://vault.internal.example.com"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes/west"
          role: "eso-west-role"
          serviceAccountRef:
            name: "external-secrets-sa"

TLS憑證多叢集分發

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: wildcard-tls
  namespace: ingress-nginx
spec:
  refreshInterval: 24h
  secretStoreRef:
    name: vault-east
    kind: SecretStore
  target:
    name: wildcard-tls
    creationPolicy: Owner
    template:
      type: kubernetes.io/tls
  data:
    - secretKey: tls.crt
      remoteRef:
        key: secret/data/shared/tls/wildcard
        property: cert
    - secretKey: tls.key
      remoteRef:
        key: secret/data/shared/tls/wildcard
        property: key

Sealed Secrets多叢集主密鑰同步

# 匯出主密鑰
kubectl get secret -n kube-system sealed-secrets-key \
  -o yaml > sealed-secrets-master-key.yaml

# 在目標叢集匯入
kubectl apply -f sealed-secrets-master-key.yaml -n kube-system

# 重啟Controller載入密鑰
kubectl rollout restart deployment/sealed-secrets-controller -n kube-system

Pattern 6: 稽核與合規

DevOps GitOps密鑰管理必須滿足稽核要求:誰在什麼時候存取了什麼密鑰。2026年合規標準(SOC2、ISO 27001、等保2.0)都要求密鑰存取可追溯。

Vault稽核日誌

# 啟用Vault稽核日誌
audit {
  type = "file"
  options = {
    file_path = "/vault/audit/audit.log"
    mode      = "0600"
  }
}

# 啟用Syslog稽核
audit {
  type = "syslog"
  options = {
    facility = "AUTH"
    tag      = "vault"
    address  = "syslog.internal:514"
  }
}

ESO存取日誌

apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: audited-vault
  annotations:
    audit.external-secrets.io/enabled: "true"
    audit.external-secrets.io/log-access: "true"
spec:
  provider:
    vault:
      server: "https://vault.internal.example.com"
      path: "secret"
      version: "v2"
      auth:
        kubernetes:
          mountPath: "kubernetes"
          role: "eso-audited-role"
          serviceAccountRef:
            name: "external-secrets-sa"

Kubernetes稽核策略

apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  - level: RequestResponse
    resources:
      - group: ""
        resources: ["secrets"]
    namespaces: ["production", "staging"]
    omitStages:
      - RequestReceived

  - level: Metadata
    resources:
      - group: ""
        resources: ["secrets"]
    namespaces: ["default"]
    omitStages:
      - RequestReceived

  - level: RequestResponse
    resources:
      - group: "external-secrets.io"
        resources: ["externalsecrets", "secretstores"]
    omitStages:
      - RequestReceived

密鑰存取監控

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: restrict-secret-access
spec:
  validationFailureAction: Audit
  background: true
  rules:
    - name: block-default-sa-secret-access
      match:
        resources:
          kinds: [Pod]
      validate:
        message: "禁止使用default ServiceAccount存取Secret"
        pattern:
          spec:
            serviceAccountName: "!default"

    - name: require-secret-annotations
      match:
        resources:
          kinds: [Secret]
          names: ["db-*", "api-*", "tls-*"]
      validate:
        message: "生產密鑰必須標註owner和expiry"
        pattern:
          metadata:
            annotations:
              owner: "?*"
              expiry: "?*"

5個常見坑及解決方案

坑1: Sealed Secrets主密鑰遺失

現象:叢集重建後,所有SealedSecret無法解密,Controller報錯failed to unseal。

原因:Sealed Secrets使用非對稱加密,私鑰只在叢集內存在。叢集銷毀後私鑰遺失。

解決方案:

# 1. 定期備份主密鑰(自動化)
kubectl get secret -n kube-system \
  sealed-secrets-key -o yaml | \
  sops --encrypt --age age1abc123xyz456... \
  /dev/stdin > sealed-secrets-key.enc.yaml

# 2. 將加密後的密鑰存入另一個Git儲存庫
git add sealed-secrets-key.enc.yaml
git commit -m "backup: sealed secrets master key $(date +%Y%m%d)"

# 3. 恢復流程
sops --decrypt sealed-secrets-key.enc.yaml | \
  kubectl apply -f -

坑2: ExternalSecret刷新延遲導致Pod啟動失敗

現象:新部署的Pod因Secret不存在而啟動失敗,ESO還沒來得及從Vault拉取密鑰。

解決方案:

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
spec:
  refreshInterval: 5m
  target:
    creationPolicy: Owner
    template:
      type: Opaque
  dataFrom:
    - extract:
        key: production/app-secrets
# 確保ESO在應用部署前已同步
kubectl wait --for=condition=Ready \
  externalsecret/app-secrets -n production --timeout=120s

坑3: SOPS密鑰輪換後舊密文無法解密

現象:團隊更換Age密鑰後,舊的加密檔案無法解密。

解決方案:

# 使用多密鑰加密——新舊密鑰共存
sops --encrypt \
  --age age1NEWKEY...,age1OLDKEY... \
  --in-place secret.yaml

# 或使用.sops.yaml管理密鑰組
sops --decrypt --age age1OLDKEY... secret.yaml | \
  sops --encrypt --age age1NEWKEY... \
  --filename-override secret.yaml /dev/stdin > secret_new.yaml

坑4: 密鑰洩露後無法快速撤銷

現象:API Key洩露,但SealedSecret需要重新加密提交,撤銷窗口太長。

解決方案:

# ESO方案:直接在Vault中禁用舊密鑰
vault kv metadata put -delete-version-after=0s \
  secret/data/production/api-key

# Sealed Secrets方案:使用Controller緊急刪除
kubectl delete secret api-key -n production

# 通用方案:網路層阻斷
kubectl apply -f - <<EOF
apiVersion: networkpolicies.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: block-compromised
  namespace: production
spec:
  podSelector: {}
  policyTypes: [Egress]
  egress:
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
              - compromised-api.example.com/32
EOF

坑5: 多團隊共享密鑰導致權限失控

現象:多個團隊使用同一個Vault Token存取密鑰,無法區分誰做了什麼。

解決方案:

# 按團隊建立Vault策略
path "secret/data/team-a/*" {
  capabilities = ["read", "list"]
}

path "secret/data/team-b/*" {
  capabilities = ["read", "list"]
}

# 交叉存取顯式拒絕
path "secret/data/team-a/*" {
  capabilities = ["deny"]
}
# 綁定到team-b的角色

# 使用Vault命名空間隔離
namespace "team-a" {
  path "secret/*" {
    capabilities = ["read", "list", "create", "update"]
  }
}

10個常見報錯排查

1. failed to unseal: no private key found

kubectl get secrets -n kube-system -l sealedsecrets.bitnami.com/sealed-secrets-key
kubectl apply -f sealed-secrets-key-backup.yaml -n kube-system
kubectl rollout restart deployment/sealed-secrets-controller -n kube-system

2. ExternalSecret "not ready": could not get secret data

kubectl describe secretstore vault-backend -n production
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets
kubectl auth can-i get secret -n production \
  --as=system:serviceaccount:production:external-secrets-sa

3. sops error decrypting: could not find a matching key

age-keygen -y age.key
cat .sops.yaml
sops --decrypt --age age1YOURKEY... secret.yaml

4. SealedSecret "sealed-secrets" is invalid: metadata.name

kubeseal --scope strict --namespace production \
  --name db-credentials < secret.yaml > sealed.yaml

5. Vault seal status: sealed

vault status
vault operator unseal <key1>
vault operator unseal <key2>

6. refreshInterval: cannot unmarshal

spec:
  refreshInterval: 15m

7. kubeseal: error: failed to get certificate

kubectl get deployment sealed-secrets-controller -n kube-system
kubectl get secret -n kube-system sealed-secrets-key
kubeseal --fetch-cert > sealed-secrets-cert.pem
kubeseal --cert sealed-secrets-cert.pem < secret.yaml

8. ExternalSecret status: SecretSyncedError

kubectl describe externalsecret db-credentials -n production
vault kv list secret/production/
vault policy read eso-role

9. SOPS: mac mismatch: file has been modified

sops --decrypt secret.yaml > secret_plain.yaml
sops --encrypt --age age1YOURKEY... secret_plain.yaml > secret.yaml

10. Kubernetes: secret "xxx" not found

kubectl get externalsecret -n production
kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets --tail=100
kubectl annotate externalsecret db-credentials \
  force-sync=$(date +%s) -n production --overwrite

進階優化技巧

1. Git Pre-commit Hook防止明文密鑰提交

#!/bin/bash
# .git/hooks/pre-commit

STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.yaml$|\.yml$|\.env$')

for FILE in $STAGED_FILES; do
  if grep -qE 'kind:\s*Secret' "$FILE" 2>/dev/null; then
    if ! grep -qE 'kind:\s*SealedSecret|encryptedData|sops:' "$FILE" 2>/dev/null; then
      echo "ERROR: 發現未加密的Secret檔案: $FILE"
      exit 1
    fi
  fi

  if grep -qiE '(password|secret|api.key|token)\s*[:=]\s*["\x27]?[A-Za-z0-9+/=]{16,}' "$FILE" 2>/dev/null; then
    if ! grep -qE 'ENC\[|sops:|encryptedData' "$FILE" 2>/dev/null; then
      echo "WARNING: 疑似明文密鑰: $FILE"
    fi
  fi
done

exit 0

2. ArgoCD整合密鑰管理

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app-with-secrets
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/gitops-manifests.git
    targetRevision: main
    path: overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

3. 密鑰模板引擎

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-config
  namespace: production
spec:
  refreshInterval: 15m
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: app-config
    template:
      type: Opaque
      engineVersion: v2
      data:
        DATABASE_URL: "postgresql://{{ .db_user }}:{{ .db_pass }}@{{ .db_host }}:5432/{{ .db_name }}?sslmode=require"
        REDIS_URL: "redis://:{{ .redis_pass }}@{{ .redis_host }}:6379/0"
        JWT_SECRET: "{{ .jwt_secret }}"
        CONFIG_JSON: |
          {
            "database": {
              "host": "{{ .db_host }}",
              "port": 5432,
              "name": "{{ .db_name }}"
            },
            "redis": {
              "host": "{{ .redis_host }}"
            }
          }
  data:
    - secretKey: db_user
      remoteRef:
        key: secret/data/production/database
        property: username
    - secretKey: db_pass
      remoteRef:
        key: secret/data/production/database
        property: password
    - secretKey: db_host
      remoteRef:
        key: secret/data/production/database
        property: host
    - secretKey: db_name
      remoteRef:
        key: secret/data/production/database
        property: dbname
    - secretKey: redis_pass
      remoteRef:
        key: secret/data/production/redis
        property: password
    - secretKey: redis_host
      remoteRef:
        key: secret/data/production/redis
        property: host
    - secretKey: jwt_secret
      remoteRef:
        key: secret/data/production/auth
        property: jwt_secret

4. 密鑰健康檢查

apiVersion: batch/v1
kind: CronJob
metadata:
  name: secret-health-check
  namespace: security
spec:
  schedule: "0 8 * * 1"
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: secret-checker
          containers:
            - name: checker
              image: bitnami/kubectl:1.30
              command:
                - /bin/bash
                - -c
                - |
                  echo "=== 密鑰健康檢查 $(date) ==="
                  echo "--- 檢查即將過期的TLS憑證 ---"
                  kubectl get secrets --all-namespaces \
                    -o json | jq -r '.items[] |
                    select(.type=="kubernetes.io/tls") |
                    "\(.metadata.namespace)/\(.metadata.name)"' | \
                  while read secret; do
                    ns=$(echo $secret | cut -d/ -f1)
                    name=$(echo $secret | cut -d/ -f2)
                    cert=$(kubectl get secret $name -n $ns \
                      -o jsonpath='{.data.tls\.crt}' | base64 -d)
                    expiry=$(echo "$cert" | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
                    if [ -n "$expiry" ]; then
                      days=$(( ($(date -d "$expiry" +%s) - $(date +%s)) / 86400 ))
                      if [ $days -lt 30 ]; then
                        echo "WARNING: $secret 憑證將在 ${days} 天後過期"
                      fi
                    fi
                  done
                  echo "--- 檢查ExternalSecret同步狀態 ---"
                  kubectl get externalsecrets --all-namespaces \
                    -o json | jq -r '.items[] |
                    select(.status.conditions[]?.type=="Ready" and
                           .status.conditions[]?.status!="True") |
                    "\(.metadata.namespace)/\(.metadata.name): NOT READY"'
          restartPolicy: OnFailure

對比分析:Sealed Secrets vs External Secrets vs SOPS

維度 Sealed Secrets External Secrets Operator SOPS
加密方式 非對稱加密(叢集公鑰) 不加密Git內容 對稱/非對稱加密
Git儲存內容 加密密文(SealedSecret) 引用配置(ExternalSecret) 加密密文(值加密)
外部依賴 無(自包含) Vault/AWS/GCP等 無(自包含)
密鑰輪換 需重新加密 天然支援 需重新加密
多叢集 需同步主密鑰 每叢集獨立SecretStore 共享加密密鑰
Git Diff友好 否(密文不可讀) 是(引用可讀) 是(鍵可讀值加密)
離線解密 否(需叢集私鑰) 否(需外部服務) 是(本機有密鑰即可)
學習曲線 低 中 中
維運複雜度 低 高(需維護Vault) 低
稽核能力 Git歷史 Vault稽核日誌 Git歷史
動態密鑰 不支援 支援(Vault動態憑證) 不支援
適用規模 小中型團隊 中大型企業 任意規模
推薦場景 簡單GitOps 企業級密鑰管理 多格式加密

選型決策樹

是否已有Vault/AWS Secrets Manager?
├── 是 → External Secrets Operator
│        └── 需要動態密鑰?→ ESO + Vault動態憑證
└── 否 → 是否需要Git儲存加密密文?
         ├── 是 → 是否需要Git Diff友好?
         │        ├── 是 → SOPS + Age
         │        └── 否 → Sealed Secrets
         └── 否 → 是否需要動態密鑰?
                  ├── 是 → 部署Vault + ESO
                  └── 否 → Sealed Secrets

線上工具推薦


總結:DevOps GitOps密鑰管理沒有銀彈,但有最佳實踐路徑。小團隊從Sealed Secrets起步,零外部依賴即可實現GitOps密鑰安全;中大型企業選擇External Secrets Operator + Vault,天然支援密鑰輪換和動態憑證;需要多格式加密和Git Diff友好時,SOPS + Age是最佳選擇。2026年的核心原則:零明文儲存、自動輪換、可稽核可追溯。記住——密鑰洩露不是「會不會」的問題,而是「什麼時候」的問題。


相關文章:

外部參考:

#DevOps#GitOps#密钥管理#Sealed Secrets#External Secrets#SOPS#2026#DevOps