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