DevOps GitOps密鑰管理:從Sealed Secrets到External Secrets的6種生產模式
把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
線上工具推薦
- Base64編解碼:/zh-TW/encode/base64 — 編解碼K8s Secret和SealedSecret資料
- RSA密鑰生成:/zh-TW/encode/rsa — 生成RSA密鑰對用於SOPS GPG加密
- Hash計算:/zh-TW/encode/hash — 計算密鑰指紋和校驗值
總結:DevOps GitOps密鑰管理沒有銀彈,但有最佳實踐路徑。小團隊從Sealed Secrets起步,零外部依賴即可實現GitOps密鑰安全;中大型企業選擇External Secrets Operator + Vault,天然支援密鑰輪換和動態憑證;需要多格式加密和Git Diff友好時,SOPS + Age是最佳選擇。2026年的核心原則:零明文儲存、自動輪換、可稽核可追溯。記住——密鑰洩露不是「會不會」的問題,而是「什麼時候」的問題。
相關文章:
- GitOps與ArgoCD生產實踐 — ArgoCD完整部署自動化指南
- GitOps與Flux CD生產實踐 — Flux CD持續交付實戰
- Docker容器安全加固 — 容器8層防禦體系
外部參考:
本站提供瀏覽器本地工具,免註冊即可試用 →