深色模式
External Secrets Operator 外部密钥
摘要:本文面向平台工程师,讲解如何用 External Secrets Operator(ESO,CNCF Sandbox)把 AWS Secrets Manager、HashiCorp Vault、GCP Secret Manager 等外部密钥源同步为原生 K8s Secret,覆盖 CRD、认证、刷新、模板与排障。覆盖版本:ESO 稳定 API
external-secrets.io/v1,Kubernetes v1.28+。
适用版本与前提
- ESO:最新版(CRD 最低支持 K8s 1.16;本文化以
external-secrets.io/v1为准) - Kubernetes:v1.28+
- 工具:helm、kubectl
- 前提:已有外部密钥后端(Vault / AWS SM / GCP SM 等)及访问凭据
背景与问题
原生 Secret 有三难:默认不加密、无自动轮换、跨集群难共享、缺访问审计。团队通常已用 Vault 或云密钥管理器。ESO 的答案是:真相源在集群外,Git 只存引用不存值——ExternalSecret 清单可安全入库,真实密钥由控制器周期性拉取并写为原生 Secret,工作负载照常消费。
核心概念与资源模型
三个核心 CRD:
SecretStore(命名空间级):定义如何连后端、如何认证。ClusterSecretStore(集群级):跨命名空间复用,适合全局后端。ExternalSecret(命名空间级):声明“从哪个 Store 取哪些 key,映射成哪个 K8s Secret,多久刷新一次”。
apiVersion 说明
官方 Getting Started 当前使用 apiVersion: external-secrets.io/v1(GA)。社区部分旧文用 v1beta1,仍可用但建议向 v1 迁移。[版本相关:以目标 ESO 版本为准]
生产实践
步骤 1:安装
bash
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
-n external-secrets --create-namespace
# CRD 体积已达 256KB 上限,需用 server-side apply 安装
kubectl apply -f "https://raw.githubusercontent.com/external-secrets/external-secrets/<版本>/deploy/crds/bundle.yaml" --server-side1
2
3
4
5
2
3
4
5
步骤 2:AWS Secrets Manager(ClusterSecretStore + JWT/IAM)
ESO ServiceAccount 经 OIDC 联盟到 IAM Role(IRSA),无长期静态密钥:
yaml
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: aws-secrets
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
namespace: external-secrets1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
步骤 3:ExternalSecret 同步
yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets
kind: ClusterSecretStore
target:
name: db-credentials
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: production/database
property: username
- secretKey: password
remoteRef:
key: production/database
property: password1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
creationPolicy: Owner 表示 ESO 拥有该 Secret 生命周期。refreshInterval 控制轮询频率,默认 1h;高轮换密钥可降到 5m,永不轮换设 0(仅同步一次)。
步骤 4:整段同步与模板
dataFrom.extract 拉取整段 key-value;template 可拼装复合格式(连接串、JSON):
yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: postgres-connection
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets
kind: ClusterSecretStore
target:
name: postgres-connection
template:
engineVersion: v2
data:
DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@db.prod:5432/payments?sslmode=require"
data:
- secretKey: username
remoteRef: { key: production/database, property: username }
- secretKey: password
remoteRef: { key: production/database, property: password }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
HashiCorp Vault 示例
yaml
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: vault-backend
namespace: production
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "production-app"
serviceAccountRef:
name: vault-auth1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
多租户隔离
ClusterSecretStore 全局共享;做租户隔离时用命名空间级 SecretStore,并让后端 Role 仅授权该命名空间路径(如 Vault role 仅允许 secret/payments/*),避免越权读取。
验证
bash
kubectl describe externalsecret db-credentials -n production
# 期望 Condition: Ready=True, Reason=SecretSynced
kubectl get secret db-credentials -n production -o yaml1
2
3
2
3
回滚与清理
bash
# 暂停同步:删除 ExternalSecret,ESO 依 creationPolicy 决定是否保留 Secret
kubectl delete externalsecret db-credentials -n production
# 彻底卸载
kubectl get SecretStores,ClusterSecretStores,ExternalSecrets --all-namespaces
helm delete external-secrets -n external-secrets1
2
3
4
5
2
3
4
5
删除顺序
卸载 ESO 前须先删所有 ExternalSecret/SecretStore,否则残留 CRD 与孤儿 Secret 难以追溯。[版本相关:CRD 256KB 限制使重装需 server-side apply]
故障排查
| 现象 | 根因 | 解决 |
|---|---|---|
| SecretSyncedError | 后端认证失败 | 查 SecretStore auth、IAM/Vault Role、网络 |
| 轮换后 Secret 不变 | refreshInterval 未到 | 缩短间隔或手动触发 |
| 值缺字段 | remoteRef.property 错 | 核对后端 key/property |
| CRD 安装失败 | 256KB 限制 | 用 --server-side apply |
安全与合规
- ESO 只是“搬运工”:写入的原生 Secret 仍受本站 Secret/KMS 篇的 etcd 加密与 RBAC 约束。
- 优先用无静态密钥的认证(IRSA / Workload Identity / Vault Kubernetes auth),避免“用 Secret 去拉 Secret”的悖论。
refreshInterval是自动轮换基础:后端值变化后,K8s Secret 在间隔内刷新,应用重启或热加载后生效。
替代方案与权衡
- Secrets Store CSI Driver:把外部密钥直接挂载进 Pod(不经原生 Secret),适合不想落 Secret 的场景。
- Sealed Secrets / SOPS:解决 Git 中加密存储,而非对接外部密钥源。
- 原生 Secret + KMS:轻量但缺轮换与审计。
参考资料
- Getting started - External Secrets Operator 官方文档,访问日期:2026-10-08。
- External Secrets Operator: Vault and Cloud - K8s Recipes,访问日期:2026-10-08。
- External Secrets Operator vs Sealed Secrets vs SOPS - BigIron,访问日期:2026-10-08。
- External Secrets Operator 实操 - SystemShardening,访问日期:2026-10-08。