在 Kubernetes 里手写 Secret 是运维最大的隐患之一:密钥散落在 YAML、Git、镜像里,轮转靠人工,泄露难追溯。External Secrets Operator(ESO)把云厂商密钥托管平台(AWS Secrets Manager、阿里云 Secrets Manager、HashiCorp Vault 等)的密钥自动同步为 K8s Secret,让应用只读、平台托管,彻底把”密钥从代码里赶出去”。我们曾遇到一次真实事故:某服务把 RDS 密码写进 ConfigMap 提交到内部 Git,仓库被无意分享后数据库被拖库——如果当时用 ESO,密钥根本不会出现在仓库里。
一、为什么需要 External Secrets Operator
原生 Secret 用 base64 编码,那不是加密。一旦仓库被拉取、镜像被导出,密钥就裸奔。更糟的是轮转:改一个数据库密码要手动 kubectl create secret 再滚动重启,出错是常态。ESO 的核心价值是”单一事实源”——密钥只存在于托管平台,K8s 里只是它的只读副本,上游一改,集群自动跟着变。
| 维度 | 手写 Secret | External Secrets Operator |
|---|---|---|
| 密钥存储 | 明文 base64 进 Git | 只在云厂商密钥库 |
| 轮转 | 人工重建 + 滚动重启 | 周期自动同步 |
| 权限 | 谁有 YAML 谁可见 | 按 IAM/RBAC 细粒度 |
| 审计 | 几乎无 | 平台侧完整日志 |
二、核心概念:SecretStore 与 ExternalSecret
ESO 用两个 CRD 把”去哪取”和”取什么”拆开:
- SecretStore:定义”从哪里取”——provider 类型(aws、alibaba、vault…)加鉴权方式。集群级用
ClusterSecretStore,全集群共享一份后端配置。 - ExternalSecret:定义”取哪个密钥、同步成什么名字、多久刷新一次”。它引用某个 SecretStore,把远端值映射成本地 K8s Secret。
三、快速上手:对接 AWS Secrets Manager
用 Helm 安装控制器(推荐固定版本,避免自动升级引入破坏性变更):
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets external-secrets/external-secrets \
-n external-secrets --create-namespace \
--set installCRDs=true \
--version 0.9.20
定义一个 ClusterSecretStore,用 IRSA(IAM Roles for Service Accounts)而非长期 AK,把权限收敛到最小:
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: aws-secretsmanager
spec:
provider:
aws:
service: SecretsManager
region: ap-northeast-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
namespace: external-secrets
再写一个 ExternalSecret,把 prod/db/password 同步成名为 db-credentials 的 Secret:
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: app
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secretsmanager
kind: ClusterSecretStore
target:
name: db-credentials
creationPolicy: Owner
data:
- secretKey: password
remoteRef:
key: prod/db/password
应用后查看状态,Synced 即成功:
kubectl apply -f externalsecret.yaml
kubectl -n app get externalsecret db-credentials
# NAME STORE REFRESH STATUS AGE
# db-credentials aws-secretsmanager 1h Synced 12s
kubectl -n app get secret db-credentials -o jsonpath='{.data.password}' | base64 -d
四、对接阿里云与其他 Provider
阿里云 Secrets Manager 同样走 IRSA 思路(用 RRSA + 临时 STS 凭据),避免把 AccessKey 写进集群。SecretStore 配置如下:
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: alibaba-secretsmanager
spec:
provider:
alibaba:
region: cn-hangzhou
auth:
rrsa:
oidcProviderArnRef:
name: cluster-oidc-arn
oidcProviderARN: acs:ram::123456:oidc-provider/ack-rrsa
ramRoleArnRef:
name: eso-role-arn
ramRoleARN: acs:ram::123456:role/eso-reader
provider 还支持 Vault、GCP Secret Manager、Azure Key Vault、1Password、HashiCorp Vault 动态密钥等几十种后端,配置结构基本一致,迁移成本低。
五、刷新策略与密钥轮转
默认 refreshInterval: 1h 周期拉取。上游改了值,最多 1 小时后集群生效。需要立即生效时,给 ExternalSecret 打注解强制触发:
kubectl -n app annotate externalsecret db-credentials \
external-secrets.io/trigger=reconcile
| 参数 | 含义 | 建议 |
|---|---|---|
| refreshInterval | 同步周期 | 高频变更 15m,稳定值 1h |
| creationPolicy | Secret 归属 | Owner(ESO 托管删除) |
| target.template | 自定义 Secret 结构 | 多 key 合并时用 |
| dataFrom | 整库导入 | 一次性搬多个 key |
注意:ESO 只负责”把值放进 Secret”,应用是否热加载取决于自己。像 Prometheus 这类支持信号重载的组件能无缝生效;不支持热加载的需配合滚动重启或 Reloader 类工具。
六、生产实践:多租户与权限边界
- ClusterSecretStore 共享、ExternalSecret 分散:一份后端配置,各命名空间按需取,避免重复定义凭据。
- 网络隔离:用 Cilium NetworkPolicy 限制 ESO 控制器只够访问密钥 API,缩小爆炸半径。
- RBAC 收敛:禁止业务命名空间编辑
ClusterSecretStore,防止越权指向别的后端。 - 防误删:
creationPolicy: Owner时删 ExternalSecret 会连带删 Secret;跨团队共享的用Orphan更安全。
七、用 dataFrom 批量导入整库密钥
当一组密钥都在同一个前缀下,逐个写 data 太啰嗦。用 dataFrom 可以一次把整段(find by tag 或 path)全部同步进来,Secret 里自动生成对应 key:
spec:
target:
name: app-all-secrets
dataFrom:
- find:
name:
regexp: "^prod/app/.*"
tags:
- env: production
conversionStrategy: Default
批量导入适合”先把密钥搬进来跑通、再逐步精细化”的迁移期;上线稳定后建议收口到显式 data 列表,避免无意中把不该暴露的 key 也同步进集群。
八、和 GitOps 协同(Argo CD)
ESO 与 GitOps 天然互补:Git 里只放 ExternalSecret 清单(不含任何真实值),GitHub Actions 或 Argo CD 把它们同步到集群,真实密钥由控制器在运行时拉取。这样 Git 仓库可以放心公开评审,密钥永不进版本库。注意 Argo CD 默认会比对 Secret 内容,建议把 ExternalSecret 设为 managed-by ESO、在 Argo CD 里忽略生成的 Secret,避免被误判为漂移。
# argocd-cm: 让 Argo CD 忽略 ESO 生成的 Secret
resource.customizations.ignoreResourceUpdates.secret: |
- jsonPointers:
- /data
- /metadata/annotations
九、与 Vault / Sealed Secrets 怎么选
| 方案 | 密钥来源 | 适用场景 |
|---|---|---|
| External Secrets Operator | 云厂商/外部密钥库 | 已用 AWS/Aliyun/Vault 托管密钥 |
| Sealed Secrets | 加密进 Git | 想 GitOps 但无外部密钥库 |
| CSI Secrets Store | 挂载为卷 | 需文件形态、不进环境变量 |
| Vault Injector | Vault Agent 注入 | 已在用 Vault 动态密钥 |
没有银弹:已经在用外部密钥平台的,ESO 是最小改动;纯 GitOps 且不想引云的,Sealed Secrets 更轻。两者也可并存。
十、常见误区与排障
| 现象 | 根因 | 处理 |
|---|---|---|
| SecretNotFound | 远端 key 不存在 | 核对 key 拼写与区域 |
| Forbidden/AccessDenied | IRSA 角色权限不足 | 补 IAM 读密钥策略 |
| SecretStore 引用失败 | kind 写错 | ClusterSecretStore 必须写 kind |
| 值一直不更新 | refreshInterval 过长 | 缩短周期或手动触发 |
| 同步超时 | 网络/限频 | 查 K8s 网络策略与配额 |
十一、密钥轮转后自动重启:Reloader 实战
很多老旧应用不支持热加载:ESO 把 Secret 更新了,Pod 里的环境变量还是旧值,要等人去滚动重启。stakater/Reloader 监听 Secret 变动,给引用它的工作负载自动打注解触发滚动重启,闭环密钥轮转。如果只同步 Secret 而不重启进程,应用读到的永远是挂载时的旧副本,轮转就失去了意义——Reloader 正是补齐这最后一环的关键组件。
helm install reloader stakater/reloader \
-n reloader --create-namespace \
--set reloader.watchGlobally=false
# 在需要自动重启的工作负载上加注解
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
reloader.stakater.com/auto: "true"
spec:
template:
metadata:
annotations:
reloader.stakater.com/search: "true"
注意轮转节奏:refreshInterval 过短又赶上上游频繁改值,会造成 Pod 反复重启抖动。建议把高危密钥的刷新周期设为 1h,并配合 revisionHistoryLimit 控制历史版本数,出问题时能快速回滚。
十二、生产落地检查清单
- 用 IRSA/临时凭据,绝不写长期 AK 进集群;
- 固定 ESO 版本,开启 installCRDs;
- ClusterSecretStore 收敛 RBAC,禁止业务命名空间改;
- refreshInterval 按变更频率设定,关键密钥加手动触发预案;
- 应用侧确认是否支持热加载,否则配 Reloader;
- 用网络策略限制控制器出网,缩小泄露面;
- 迁移前先用
dataFrom灰度验证单 key,再全量; - GitOps 场景在 Argo CD 忽略生成的 Secret,避免误判漂移。
把密钥托管交给专业平台、把同步交给 ESO,K8s 里的 Secret 从此只是”影子”。配合 Docker 与 GitHub Actions 的流水线,密钥从开发到生产的流转链路可以做到全程不落盘、可审计。这才是云原生时代该有的密钥治理姿势。



