说明:
本手册面向使用 TKE External Secrets Operator(ESO)+ 腾讯云凭据管理服务(Secrets Manager,简称 SSM)的用户,全量覆盖:
ExternalSecret:从腾讯云 SSM 拉取凭据到 K8s。
PushSecret:把 K8s Secret 反向同步到 SSM。
Generators:动态生成腾讯云临时凭证(STS)/ TCR 镜像仓库访问令牌。
1. 功能简介
能力 | 解决什么问题 | 对标的腾讯云原生操作 |
ExternalSecret | 让腾讯云 SSM 成为集中事实源,K8s Secret 自动从 SSM 拉取并保持同步;替换 tccli ssm GetSecretValue 的人工脚本流程。 | SSM 控制台,进入凭据管理系统页面,查看凭据值 |
PushSecret | 让 K8s Secret 成为事实源,自动同步到腾讯云 SSM;替换 tccli ssm CreateSecret/PutSecretValue 的人工脚本流程。 | SSM 控制台,进入凭据管理系统页面,新建凭据或更新凭据值 |
TencentSTSSessionToken Generator | 让集群内工作负载使用短时临时 AKSK 访问腾讯云,避免长期 AKSK 落盘。 | 访问管理 CAM 控制台,进入角色页面,执行 AssumeRole 操作 |
TencentTCRAccessToken Generator | 自动为 K8s 工作负载颁发 TCR 实例访问令牌,免去手动 docker login 与定期轮转令牌。 | TCR 控制台,进入实例详情页的访问凭证页面,创建临时或长期令牌 |
注意事项
三者都使用同一个
SecretStore/ClusterSecretStore Provider 体系,差异仅在 CRD 类型。所有 yaml 都可 kubectl apply -f 直接生效。关于 yaml 中出现的多个
apiVersion,本手册示例涉及三个不同的 API group,它们各自的 stable 版本独立演进,互不影响:API group | 当前 stable 版本 | 涉及的资源 |
external-secrets.io | v1 | SecretStore / ClusterSecretStore / ExternalSecret / ClusterExternalSecret |
external-secrets.io | v1alpha1 | PushSecret |
generators.external-secrets.io | v1alpha1 | TencentSTSSessionToken / TencentTCRAccessToken / ClusterGenerator |
kubernetes.external-secrets.io | v1alpha1 | PushSecretBackend |
在 ExternalSecret 实操、PushSecret 实操、Generators 实操 的示例里可以看到:核心的
ExternalSecret 用 external-secrets.io/v1,而 PushSecret 实操 的 PushSecret 用 external-secrets.io/v1alpha1、Generators 实操 的 Generator 用 generators.external-secrets.io/v1alpha1——这不是文档不一致,而是这三类资源在上游 ESO 的版本进度本来就不同。以下章会在每个 yaml 头部直接给出对应的 apiVersion,按示例 apply 即可。如果存量 yaml 仍在使用
external-secrets.io/v1beta1(自 ESO chart v1.0.0 起已升级为 v1),把 apiVersion 改为 v1 即可,字段定义完全兼容。2. 前置条件
1. TKE 集群已安装 ESO(推荐 chart 版本 ≥
v1.0.x):2. 腾讯云账号已开通 SSM 服务,并已选定地域(如
ap-guangzhou)。3. 网络可达:TKE 集群所在 VPC 需可访问腾讯云 API。公网部署默认即可;仅放通 VPC 内网域名(
*.internal.tencentcloudapi.com)的集群必须给 ESO controller 注入下列 5 个环境变量,否则 ExternalSecret 实操、PushSecret 实操、Generators 实操 的相关链路都会出现 dial tcp ... i/o timeout:环境变量 | 影响范围 | 推荐值(VPC 内网) | 默认值(公网) |
TENCENT_SSM_ENDPOINT | SSM Provider 取/推 secret | ssm.internal.tencentcloudapi.com | ssm.tencentcloudapi.com |
TENCENT_STS_ENDPOINT | STS Generator / SecretStore 角色扮演 | sts.internal.tencentcloudapi.com | sts.cloud.tencent.com |
TENCENT_TAG_ENDPOINT | R2 标签解析(DescribeResourceTags) | tag.internal.tencentcloudapi.com | tag.tencentcloudapi.com |
TENCENT_CAM_ENDPOINT | PushSecret 之 CAM Policy 管理 | cam.internal.tencentcloudapi.com | cam.tencentcloudapi.com |
TENCENT_TCR_ENDPOINT | TCR Generator 取镜像仓库临时 token | tcr.internal.tencentcloudapi.com | tcr.tencentcloudapi.com |
kubectl set env 一次性补打(已部署集群打补丁):kubectl -n kube-system set env deployment/external-secrets \\TENCENT_SSM_ENDPOINT=ssm.internal.tencentcloudapi.com \\TENCENT_STS_ENDPOINT=sts.internal.tencentcloudapi.com \\TENCENT_TAG_ENDPOINT=tag.internal.tencentcloudapi.com \\TENCENT_CAM_ENDPOINT=cam.internal.tencentcloudapi.com \\TENCENT_TCR_ENDPOINT=tcr.internal.tencentcloudapi.comkubectl -n kube-system rollout status deployment/external-secrets
自检:
kubectl -n kube-system get deploy external-secrets \\-o jsonpath='{.spec.template.spec.containers[0].env}' | jq# 期望看到上述 5 个 TENCENT_*_ENDPOINT 全部存在
公网部署可整体省略;当前 kubeconfig 已切到目标集群。
4. 使用 ExternalSecrets 组件需要 Kubernetes 版本大于等于1.19。
5. 操作系统镜像支持 x86 架构。
3. 权限准备(CAM)
本节是整个 ESO 流程的身份与授权基线,分两步走:
1. 创建获取 SSM 凭据的策略:先按业务场景准备好若干份 CAM 策略 JSON(拉取 / PushSecret / 资源策略 / TCR / Tag)。
2. 选择鉴权方式并完成授权:再决定用哪种身份去访问腾讯云(静态 AKSK / AKSK + 角色扮演 / TKE OIDC),把上一步的策略挂到对应的子账号或角色上。
说明:
控制台总入口:访问管理 CAM 控制台。
所有策略都是最小权限示例。请把示例中的
100000000001(OwnerUin,主账号)、200000000002(CreatorUin,实际创建 SSM 凭据的子账号 uin,多数生产环境就是 ESO 子账号)、ap-guangzhou、dev-*、tcr-xxxxxxxx 替换为您自己的值。关于 SSM 资源 ARN:腾讯云 SSM 的真实资源名格式为
qcs::ssm:<region>:uin/<OwnerUin>:secret/creatorUin/<CreatorUin>/<name-or-pattern>,creatorUin/<CreatorUin>/ 这一段必须显式写出,否则策略不会命中(实际线上请求会带上这一段,CAM 严格按字符串匹配)。<CreatorUin> 在 SSM 控制台凭据详情页或 tccli ssm DescribeSecret 返回的 CreateUin 字段可查。3.1 创建获取 SSM 凭据的策略
按场景挑选 / 叠加下面的策略片段即可,只读、读写、资源策略、TCR、Tag 五块按需组合。
3.1.1 基础场景:仅 ExternalSecret 拉取(最常用)
适用于只用 ExternalSecret 把 SSM 凭据同步到 K8s Secret、不做 PushSecret 的场景。
{"version": "2.0","statement": [{"effect": "allow","action": ["ssm:GetSecretValue","ssm:DescribeSecret","ssm:ListSecrets"],"resource": ["qcs::ssm:ap-guangzhou:uin/100000000001:secret/creatorUin/200000000002/dev-*"]}]}
资源格式
qcs::ssm:<region>:uin/<OwnerUin>:secret/creatorUin/<CreatorUin>/<name-or-pattern> 中 <name-or-pattern> 段支持精确名 / 通配前缀 / * 全部。不要漏掉 creatorUin/<CreatorUin>/ 这一段,否则策略不会命中真实请求资源。说明:
希望全开放调试时可以临时把
resource 改成 ["*"],生产环境请务必收敛。3.1.2 PushSecret 场景:在 3.1.1 之上追加写权限
需要从集群把 K8s Secret 反推回 SSM(PushSecret)时,在 1.1 基础上追加以下 statement。下列动作都来自 ESO 代码实际调用,未列出的不要授权:
ssm:CreateSecret:首次推送时创建凭据(写入固定 VersionId v_eso_latest)。ssm:UpdateSecret:值有变化时替换 v_eso_latest 的值(PushSecret 不会通过 PutSecretValue 增加新版本,固定只维护一个 ESO 管理的 VersionId)。ssm:DisableSecret + ssm:DeleteSecret:DeletionPolicy=Delete 清理远端时,先 disable 再 delete。{"effect": "allow","action": ["ssm:CreateSecret","ssm:UpdateSecret","ssm:DisableSecret","ssm:DeleteSecret"],"resource": ["qcs::ssm:ap-guangzhou:uin/100000000001:secret/creatorUin/200000000002/dev-*"]}
说明:
ssm:DescribeSecret 与 ssm:ListSecrets 写链路前置也会用到,但已包含在 基础场景:仅 ExternalSecret 拉取,挂在同一个子账号上即可,不需要在 P1 重复声明。关于
ssm:PutSecretValue:ESO 的 PushSecret 路径不调用它(生产代码用 UpdateSecret 替换 v_eso_latest 的值)。如果你希望保留旁路通过 tccli ssm PutSecretValue 写入额外版本(例如 §6.5 案例 E 演示金丝雀场景需要先写入 v2),请单独临时授予该 action 给运维身份,不要塞到 PushSecret 的常驻策略里。如果 PushSecret 的 metadata 里用到
tags 字段(给远端 Secret 打/改 Tags),还需要标签服务的权限。代码里 Tag 资源永远是 qcs::ssm:<region>:uin/<OwnerUin>:secret/<name>,所以 resource 可以收敛到 SSM 凭据范围:{"effect": "allow","action": ["tag:ModifyResourceTags", "tag:DescribeResourceTags"],"resource": ["qcs::ssm:ap-guangzhou:uin/100000000001:secret/creatorUin/200000000002/dev-*"]}
说明:
若 CAM 控制台对 Tag 服务校验 resource 格式时报错,可临时回退到
["*"],但生产建议保持收敛。3.1.3 PushSecret 资源策略场景:再追加 CAM 策略管理权限
注意:
ESO 实现 PushSecret
resourcePolicy 的方式不是调用 SSM 自带的 *SecretsManagerSecretPolicy 接口(腾讯云 SSM 也不直接对外暴露这套 API),而是把策略翻译成 CAM 的自定义策略对象,通过 cam:CreatePolicy / GetPolicy / UpdatePolicy / DeletePolicy / ListPolicies 在用户的 CAM 下管理 eso-ssm-* 前缀的策略。所以这里授权的是 CAM、不是 SSM。{"version": "2.0","statement": [{"effect": "allow","action": ["cam:CreatePolicy","cam:GetPolicy","cam:UpdatePolicy","cam:DeletePolicy","cam:ListPolicies"],"resource": ["*"]}]}
说明:
CAM 策略对象本身不支持按名字前缀做 resource 收敛,必须
["*"]。ESO 使用固定前缀 eso-ssm- 命名其托管的策略,建议运维定期 ListPolicies 巡检以确保不被滥用。3.1.4 TCR 场景:使用 TencentTCRAccessToken Generator
{"version": "2.0","statement": [{"effect": "allow","action": ["tcr:CreateInstanceToken"],"resource": ["qcs::tcr:ap-guangzhou:uin/100000000001:instance/tcr-xxxxxxxx"]}]}
3.1.5 场景速查
按业务用途选择需要叠加的策略片段:
3.2 选择鉴权方式并完成授权
把上一步准备好的策略挂到具体的身份上,并让 ESO 知道用哪种方式去用它。三种方式互不混用,配置形态、
SecretStore 字段、需要授权的对象都不同:对比维度 | 方式一:静态 AKSK | 方式二:AKSK + 角色扮演 | 方式三:TKE OIDC(无 AKSK 落盘) |
集群里存什么 | 子账号的长期 AKSK(K8s Secret) | 子账号的长期 AKSK(K8s Secret) | 仅一个 ServiceAccount,没有任何 AKSK |
真正访问云 API 的身份 | 子账号本人 | 被扮演的角色(临时 AKSK) | 被扮演的角色(临时 AKSK) |
挂在子账号/用户组上 | 挂在角色上(子账号只需 sts:AssumeRole) | 挂在角色上(OIDC 角色,子账号不参与) | |
凭据轮转成本 | 高(手动改 AKSK 并同步进集群) | 低(角色策略可在线改,临时凭证 自动过期) | 最低(无长期凭据) |
跨账号 | 不支持 | 支持(角色实体填对方主账号) | 不支持 |
SecretStore 形态 |
3.2.1 方式一:通过 AKSK 授权
ESO 在 SecretStore 里通过
auth.secretRef 指向 K8s Secret,里面写的就是子账号的长期 AKSK,访问 SSM 时身份就是子账号。授权步骤
步骤 1:创建子账号
1. 登录 腾讯云访问管理控制台,选择左侧导航中的用户 > 用户列表。
2. 在用户列表页面,单击新建用户。新建用户流程详情请参见 新建子用户。
注意:
已经存在子账号忽略本步骤;创建过程中务必勾选访问方式中的编程访问(生成 SecretId/SecretKey),否则步骤 3 没有可下发的 AKSK。仅勾选控制台访问无法供 ESO 使用。
步骤 2:绑定策略
1. 在 访问管理控制台 左侧导航栏中,选择用户 > 用户列表,找到上一步创建的子账号。
2. 单击子账号名称进入详情页,选择权限 tab,单击关联策略。
3. 在弹窗中勾选 §3.1 中创建的策略,单击确定完成绑定。
步骤 3:获取子账号的 AKSK
3.2.2 方式二:通过 AKSK 与角色扮演授权(AssumeRole)
ESO 在 SecretStore 里仍然用
auth.secretRef 提供子账号 AKSK,但额外填写顶级 role: "qcs::cam::uin/100000000001:roleName/eso-runtime-role" —— provider 启动时会自动调用 AssumeRole 拿临时凭证,再用临时凭证访问 SSM。真正访问云 API 的身份是角色,子账号只承担“换票据”这一个动作。授权步骤
步骤 1:创建子账号
1. 登录 腾讯云访问管理控制台,选择左侧导航中的用户 > 用户列表。
2. 在用户列表页面,单击新建用户。新建用户流程详情请参见 新建子用户。
3. 为创建的子账号赋予扮演角色的策略。详情请参见 为子账号赋予扮演角色策略。
注意:
已经存在子账号忽略本步骤;创建过程中务必勾选访问方式中的编程访问(生成 SecretId/SecretKey)。本方式中子账号的 AKSK 用于发起
AssumeRole 调用,不勾选编程访问就无法签名 STS 请求,方式二无法工作。步骤 2:绑定策略
1. 在 访问管理控制台 左侧导航栏中,选择角色,进入角色管理页面。
2. 单击新建角色,并选择腾讯云账户作为角色载体。
3. 在新建自定义角色页面,角色载体选择当前主账号,单击下一步。
4. 在配置角色策略步骤,勾选 §3.1 中创建的策略,单击下一步。
5. 在审阅步骤,输入角色名称(例如
eso-runtime-role),核对角色信息后单击完成,完成角色创建。步骤 3:获取子账号的 AKSK
3.2.3 方式三:通过 TKE OIDC 授权(无 AKSK 落盘)
通过 TKE 集群的 OIDC 身份提供商授权时,不需要在集群里存放任何长期 AKSK:ESO 控制器以 ServiceAccount 身份运行,K8s 自动为 Pod 投影出短时 OIDC Token,腾讯云 STS 凭此颁发临时凭证。该方式适用于对凭据落盘有合规要求或希望按工作负载粒度做最小授权的场景。
授权步骤
步骤 1:开启集群的 ServiceAccountIssuerDiscovery(OIDC 资源访问控制)
1. 若系统提示无法修改相关参数,请先完成服务授权:在弹出的角色管理页面查看授权策略
QcloudAccessForTKERoleInOIDCConfig,单击同意授权。2. 授权完毕后,在弹窗中同时勾选创建 CAM OIDC 提供商与创建 webhook 组件(pod-identity-webhook),并按需填写客户端 ID(选填,留空时默认为
sts.cloud.tencent.com,本指南后续示例都按默认值),单击确定。3. 返回集群基本信息页,当
ServiceAccountIssuerDiscovery 字段可再次编辑、且组件管理中 pod-identity-webhook 状态为成功即代表开启完成。4. 此时 CAM 会自动创建一个身份提供商(一般以集群 ID 命名,例如
cls-xxxxxxxx),其 Issuer URL 形如 https://<region>-oidc.tke.tencentcs.com/id/<hash>(可在集群内执行 kubectl get --raw /.well-known/openid-configuration | jq -r .issuer 查询确认)。注意:
service-account-issuer 与 service-account-jwks-uri 参数值由 TKE 按默认规则下发,不允许手工编辑。该步骤会在集群中启用 SA Token 投影,并在腾讯云侧暴露集群的 OIDC 配置端点,供 CAM 进行联合身份校验。步骤 2:在 CAM 中创建 OIDC 身份提供商(如自动创建可跳过)
如果步骤 1 没有勾选创建 CAM OIDC 提供商,需要手动创建:
提供商名称:建议与集群 ID 保持一致,例如
cls-xxxxxxxx。提供商 URL:填写步骤 1 中查询到的 Issuer URL(形如
https://<region>-oidc.tke.tencentcs.com/id/<hash>)。客户端 ID:填写
sts.cloud.tencent.com(即默认 audience,与步骤 1 留空时一致;后续 ServiceAccount Token 的 aud claim 必须与该值匹配)。公钥:可点击自动获取公钥,CAM 会从 Issuer URL 的
/.well-known/openid-configuration 拉取并缓存。保存后,记录提供商 ARN(形如
qcs::cam::uin/100000000001:oidcProvider/cls-xxxxxxxx),它在步骤 3 角色信任策略中以 身份提供商 字段被引用。步骤 3:新建 OIDC 角色并关联 §3.1 策略
在 CAM 控制台新建一个 OIDC 角色:
1. 进入 CAM 控制台角色页面,单击新建角色,选择身份提供商类型。
2. 在新建自定义角色页面填入:
身份提供商类型:
OIDC选择身份提供商:选择步骤 1/2 创建的提供商(即
$my_provider_id,一般等于集群 ID cls-xxxxxxxx)。使用条件:填写
oidc:aud,value 必须与步骤 1/2 配置的客户端 ID 严格相等(默认即 sts.cloud.tencent.com,记为 $my_pod_audience)。oidc:aud 配置多个 value 时任选其一即可。3. 单击下一步进入配置角色策略页面,勾选 §3.1 中按业务场景准备好的策略(基础读 / + PushSecret 写 / + 资源策略 / + TCR / + Tag)。
4. 单击下一步,编辑角色名称与角色描述(例如
eso-runtime-role),完成创建。5. 进入角色详情页,记录页面顶部的 RoleArn(形如
qcs::cam::uin/100000000001:roleName/eso-runtime-role,记为 $my_pod_role_arn),下一步在 ServiceAccount annotation 中要用。说明:
最终该 ServiceAccount 能访问哪些 SSM 资源、能做哪些动作,完全由挂在该角色上的策略决定。如果后续要新增/收回权限(例如增加 §3.1.2 的 PushSecret 写权限),直接在该角色的权限页关联/解除策略即可,无需改动集群侧任何资源。
步骤 4:在集群中创建 ServiceAccount 并附加 annotation
ESO 控制器(或任何使用 SA 形态认证的 Pod)所在 namespace 中,创建 ServiceAccount,并在 metadata 中带上以下三条 annotation:
apiVersion: v1kind: ServiceAccountmetadata:name: my-sanamespace: my-nsannotations:tke.cloud.tencent.com/role-arn: qcs::cam::uin/100000000001:roleName/eso-runtime-role # 步骤 3 拿到的 RoleArn ($my_pod_role_arn)tke.cloud.tencent.com/audience: sts.cloud.tencent.com # 与步骤 3 oidc:aud 严格相等 ($my_pod_audience)tke.cloud.tencent.com/providerID: cls-xxxxxxxx # 步骤 1/2 的身份提供商名 ($my_provider_id)
tke.cloud.tencent.com/role-arn:步骤 3 中创建的 OIDC 角色 ARN。tke.cloud.tencent.com/audience:Token 受众,必须与步骤 3 角色信任策略里 oidc:aud 的 value 严格相等(步骤 1 留空时默认即 sts.cloud.tencent.com)。tke.cloud.tencent.com/providerID:步骤 1/2 中的 OIDC 身份提供商名(一般等于集群 ID)。ESO 在初始化 SecretStore 时会读取这三条 annotation,使用 ServiceAccount 投影出来的 Token 调用
AssumeRoleWithWebIdentity,换取临时 SecretId / SecretKey / Token,再用其访问 SSM。整个过程没有任何 AKSK 文件落盘到容器或 K8s Secret。注意:
Token 的 audience 必须写在 ServiceAccount 的 annotation
tke.cloud.tencent.com/audience 上(由 pod-identity-webhook 在 Pod 启动时据此向 SA Token 注入对应 aud claim),而不是写在 SecretStore 的 auth.serviceAccountRef.audiences 字段——后者即使填写也会被忽略。三方等式必须严格成立:SA annotation audience ≡ CAM OIDC 提供商客户端 ID ≡ 角色信任策略 oidc:aud。临时凭证默认有效期 1 小时,ESO 会在过期前自动刷新;若手动调用 STS API,请自行处理刷新。
Pod 必须使用上述 ServiceAccount(
spec.serviceAccountName: my-sa),且 kube-apiserver 已正确投影 SA Token。4. 准备凭证 Secret
# tencent-credentials.yamlapiVersion: v1kind: Secretmetadata:name: tencent-credentialsnamespace: defaulttype: OpaquestringData:secret-id: "AKIDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"secret-key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
kubectl apply -f tencent-credentials.yaml
注意:
方式三(OIDC)不需要这一步,凭证由 SA Token 自动注入。
5. 创建 SecretStore
5.1 静态 AKSK SecretStore(对应方式一 / 方式二)
形态 A:方式一纯静态 AKSK
apiVersion: external-secrets.io/v1kind: SecretStoremetadata:name: tencent-ssm-storenamespace: defaultspec:provider:tencent:regionID: ap-guangzhouauth:secretRef:accessKeyIDSecretRef:name: tencent-credentialskey: secret-idaccessKeySecretSecretRef:name: tencent-credentialskey: secret-key
形态 B:方式二 AKSK + 角色扮演(多一个顶级
role 字段)apiVersion: external-secrets.io/v1kind: SecretStoremetadata:name: tencent-ssm-store-assumednamespace: defaultspec:provider:tencent:regionID: ap-guangzhourole: qcs::cam::uin/100000000001:roleName/eso-runtime-role # 见 §3.2.2auth:secretRef:accessKeyIDSecretRef:name: tencent-credentialskey: secret-idaccessKeySecretSecretRef:name: tencent-credentialskey: secret-key
形态 B 的
secretRef 仍然指向子账号的长期 AKSK;provider 启动时用这个 AKSK 调用 AssumeRole,之后访问 SSM 全程使用临时凭证。校验(两种形态相同):
kubectl get secretstore tencent-ssm-store -o jsonpath='{.status.conditions}'# 期望看到 type=Ready, status=True
5.2 OIDC ServiceAccount SecretStore(对应方式三)
apiVersion: external-secrets.io/v1kind: SecretStoremetadata:name: tencent-ssm-store-oidcnamespace: my-nsspec:provider:tencent:regionID: ap-guangzhouauth:serviceAccountRef:name: my-sa # 见 §3.2.3 中带三条 annotation 的 SA(providerID / role-arn / audience)
注意:
与 ESO 部分通用 Provider 不同,腾讯云 Provider 不通过 SecretStore 的
auth.serviceAccountRef.audiences 字段读取 audience;audience 由 ServiceAccount annotation tke.cloud.tencent.com/audience 提供(详见 §3.2.3 步骤 4)。audiences 字段写在这里也会被忽略,请勿误用。5.2.1 引用 OIDC SecretStore 的 ExternalSecret 完整示例
下面给出方式三场景下一个完整可复制的 ExternalSecret 示例,以便直观看到
secretStoreRef 与 OIDC SecretStore 的对接方式。remoteRef.version 字段在引用非 ESO 创建凭据时为何必填,详见 §6 章节开头的 通用提示(与认证方式无关,方式一/二/三均适用)。apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:name: oidc-pull-appnamespace: my-nsspec:refreshInterval: 1msecretStoreRef:kind: SecretStorename: tencent-ssm-store-oidc # §5.2 创建的 OIDC SecretStoretarget:name: oidc-pull-appcreationPolicy: Ownerdata:- secretKey: usernameremoteRef:key: dev-app-oidc-sample # 远端凭据名(由运维侧用 tccli 创建)version: v1 # 必填原因详见 §6 通用提示property: username # 可选:JSON property- secretKey: passwordremoteRef:key: dev-app-oidc-sampleversion: v1property: password
5.3 ClusterSecretStore:跨命名空间共享
如果你希望多个命名空间共用同一份 SSM 配置,使用
ClusterSecretStore。它是集群级资源,凭证 Secret 引用必须显式带上 namespace。说明:
如果你使用方式二/方式三,请把
spec.provider.tencent 下的 auth 字段替换为对应写法:方式二(AKSK + 角色扮演):参照 5.1 形态 B,在
auth.secretRef 同级追加 role 字段;凭证 Secret 同样需要 namespace。方式三(TKE OIDC):参照 5.2 OIDC ServiceAccount SecretStore,把
auth.secretRef 改为 auth.serviceAccountRef,并在该字段下显式声明 ServiceAccount 所在 namespace(ClusterSecretStore 必填);无需 secretRef,也不需要写 audiences 字段(audience 由 SA annotation 提供)。apiVersion: external-secrets.io/v1kind: ClusterSecretStoremetadata:name: tencent-ssm-cluster-storespec:provider:tencent:regionID: ap-guangzhouauth:secretRef:accessKeyIDSecretRef:name: tencent-credentialsnamespace: default # ClusterSecretStore 必须显式 namespacekey: secret-idaccessKeySecretSecretRef:name: tencent-credentialsnamespace: defaultkey: secret-key
校验:
kubectl get clustersecretstore tencent-ssm-cluster-store -o jsonpath='{.status.conditions}'# 期望看到 type=Ready, status=True
后续
ExternalSecret / PushSecret 引用时把 kind 改为 ClusterSecretStore:spec:secretStoreRef:name: tencent-ssm-cluster-storekind: ClusterSecretStore
5.4 SSM 行为参数:删除窗口与重试
apiVersion: external-secrets.io/v1kind: SecretStoremetadata:name: tencent-ssm-storespec:provider:tencent:regionID: ap-guangzhouprefix: "dev/" # 所有 Secret 名自动加 "dev/" 前缀,做隔离auth:secretRef:accessKeyIDSecretRef: { name: tencent-credentials, key: secret-id }accessKeySecretSecretRef: { name: tencent-credentials, key: secret-key }ssm:recoveryWindowInDays: 7 # 删除后保留 7 天可恢复,0 立即销毁networkFailureMaxRetries: 3rateLimitExceededMaxRetries: 5
参数 | 取值 | 说明 |
prefix | string | 所有 Secret 名自动加前缀,读写都生效;用于多环境/多团队隔离 |
ssm.recoveryWindowInDays | 0 或 1-365 | 删除后的恢复窗口; 0 立即永久删除 |
ssm.networkFailureMaxRetries | int | 网络抖动重试次数(指数退避) |
ssm.rateLimitExceededMaxRetries | int | API 限流重试次数(指数退避) |
6. ExternalSecret 实操
本章每个案例都按统一骨架组织:业务场景、前置条件、操作步骤、验证、清理、常见错误。
通用提示:
remoteRef.version 在引用非 ESO 创建凭据时必填(与认证方式无关,方式一 / 方式二 / 方式三均适用)ESO 自己通过
PushSecret 或回写写入的凭据使用固定 VersionId v_eso_latest,因此 version 留空也能命中。当
version 留空时,腾讯云 Provider 会先按 v_eso_latest 查询,未命中再 fallback 到 SSM 默认版本 SSM_Current。如果远端凭据是由控制台或
tccli ssm CreateSecret --VersionId v1 ... / tccli ssm PutSecretValue --VersionId xxx ... 等方式显式指定了其它 VersionId(且不存在 v_eso_latest / SSM_Current 版本),必须在 ExternalSecret 里显式声明 remoteRef.version,否则 ESO 会拿到 ResourceNotFound: can not find secrets 并归一化为 Secret does not exist。排查方法:
tccli ssm GetSecretVersionIds --SecretName <key> 列出该凭据实际存在的所有版本。6.1 案例 A 基础拉取:单字段 data
业务场景
MySQL 应用启动时需要
DB_USER / DB_PASS 两个环境变量。运维已在腾讯云 SSM 控制台创建凭据 dev-app-db,值为 JSON {"username":"admin","password":"P@ss"}。本案例把这两个字段拉取到 K8s Secret app-db,供 Deployment 通过 envFrom 注入。前置条件
已完成第 4、5.1 节(凭证 Secret + 静态 AKSK SecretStore
tencent-ssm-store)。子账号具备本案例最小权限(完整版见 §3.1.1):
远端凭据
dev-app-db 已存在(控制台或 tccli ssm CreateSecret --SecretName dev-app-db --SecretString '{"username":"admin","password":"P@ss"}')。操作步骤
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:name: app-db-esnamespace: defaultspec:refreshInterval: 1hsecretStoreRef:name: tencent-ssm-storekind: SecretStoretarget:name: app-db # 产出的 K8s Secret 名creationPolicy: Ownerdata:- secretKey: DB_USER # K8s Secret 中的键remoteRef:key: dev-app-db # SSM 中的 SecretNameproperty: username # JSON 路径;非 JSON 凭据省略- secretKey: DB_PASSremoteRef:key: dev-app-dbproperty: password
kubectl apply -f app-db-es.yaml
验证
# 1) ExternalSecret 状态:必须 Ready=True,SecretSynced=Truekubectl get externalsecret app-db-es -o jsonpath='{.status.conditions}'# 期望片段:type=Ready, status=True;以及 reason=SecretSynced# 2) 目标 Secret 已经被创建,键齐全kubectl get secret app-db -o jsonpath='{.data}' | jq 'keys'# 期望:["DB_PASS","DB_USER"]# 3) 解码后值与 SSM 完全一致kubectl get secret app-db -o jsonpath='{.data.DB_USER}' | base64 -d ; echokubectl get secret app-db -o jsonpath='{.data.DB_PASS}' | base64 -d ; echo
控制台校验:在 SSM 控制台的凭据管理系统页面搜索
dev-app-db,确认查看凭据值与上面解码内容一致。清理
kubectl delete externalsecret app-db-eskubectl get secret app-db # creationPolicy=Owner+默认 Retain:仍在;如需一并删 Secret,加 deletionPolicy=Delete(见 6.10)
常见错误
status.conditions: type=Ready status=False reason=SecretSyncedError 且消息含 AuthFailure,说明 AKSK 错误或已失效(核对 §4 凭证 Secret)。消息含
key not found / property xxx not found in secret value,说明远端 JSON 没有该 key,或大小写不匹配;用 tccli ssm GetSecretValue 直接看一下原始 JSON。ResourceNotFound,说明 SecretStore 配了 prefix,远端实际 SecretName = prefix + key,控制台搜索请带前缀。6.2 案例 B 多字段一次性导入:dataFrom.extract
业务场景
远端凭据
dev-app-db 是 JSON,包含 username/password/host/port 等若干字段。希望整份 JSON 平铺成 K8s Secret 的多个 key,避免一个字段一行 data 列。前置条件
同 6.1。远端
dev-app-db 必须是合法 JSON(否则使用 data + property 不合适,转而使用 6.6 二进制场景)。操作步骤
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:name: app-db-all-esnamespace: defaultspec:refreshInterval: 1hsecretStoreRef: { name: tencent-ssm-store, kind: SecretStore }target: { name: app-db-all }dataFrom:- extract:key: dev-app-db
kubectl apply -f app-db-all-es.yaml
验证
kubectl get externalsecret app-db-all-es -o jsonpath='{.status.conditions}'kubectl get secret app-db-all -o jsonpath='{.data}' | jq 'keys'# 期望:远端 JSON 的所有 key 全在,例如 ["host","password","port","username"]
清理
kubectl delete externalsecret app-db-all-es
常见错误
unable to unmarshal secret: invalid character ...,说明远端不是 JSON。改用 data[] + property 拉指定字段,或在 SSM 把值改成合法 JSON。产出 Secret 缺少某个键,说明远端 JSON 里该键值为
null,会被跳过;改成 "" 即可保留。6.3 案例 C 按名称前缀批量拉取:dataFrom.find.name
业务场景
App 依赖多个中间件凭据:
dev-app-db、dev-app-redis、dev-app-mq,希望它们打包成同一个 K8s Secret 并随远端一起增删。前置条件
操作步骤
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:name: app-bundle-esnamespace: defaultspec:refreshInterval: 5msecretStoreRef: { name: tencent-ssm-store, kind: SecretStore }target: { name: app-bundle }dataFrom:- find:name:regexp: "^dev-app-.*" # SSM Secret 名匹配正则
kubectl apply -f app-bundle-es.yaml
验证
kubectl get externalsecret app-bundle-es -o jsonpath='{.status.conditions}'kubectl get secret app-bundle -o jsonpath='{.data}' | jq 'keys'# 期望:每个匹配到的 SSM Secret 都成为目标 Secret 的一个键,键名 = 该 SSM 的完整 SecretName# 例如本案例期望:["dev-app-db","dev-app-mq","dev-app-redis"],对应 value 是各自远端值的整串
如果命中条目过多(默认上限 100),可在 SecretStore 用更窄的
prefix 收敛搜索范围。清理
kubectl delete externalsecret app-bundle-es
常见错误
regex compile error,请检查 regexp 转义(例如点号请写 \\\\.)。命中数量为 0,说明子账号缺
ssm:ListSecrets;或 SecretStore 的 prefix 与 regexp 重复叠加导致实际匹配名空集。6.4 案例 D 按 Tag 批量拉取:dataFrom.find.tags
业务场景
SSM 上为一批凭据打了
env=prod, team=backend 标签(运维通过控制台、tccli ssm CreateSecret --Tags、或 tccli tag AttachResourcesTag 写入),希望按 Tag 一次性拉到 K8s。前置条件
同 6.1。该案例底层用的是
ssm:ListSecrets 自带的 TagFilters 字段,不依赖 tag:DescribeResourceTags。打 tag 的 tccli 示例:# 方式 A:创建凭据时一并打 tag(推荐,最稳)tccli ssm CreateSecret --SecretName dev-app-db \\--SecretString '{"username":"admin","password":"P@ss"}' \\--Tags '[{"TagKey":"env","TagValue":"prod"},{"TagKey":"team","TagValue":"backend"}]'# 方式 B:给已有凭据补 tagtccli tag AttachResourcesTag --ServiceType ssm --ResourcePrefix secret \\--ResourceRegion ap-guangzhou --ResourceIds '["dev-app-db"]' \\--TagKey env --TagValue prod
操作步骤
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:name: app-tagged-esnamespace: defaultspec:refreshInterval: 5msecretStoreRef:name: tencent-ssm-storekind: SecretStoretarget:name: app-taggedcreationPolicy: OwnerdataFrom:- find:tags:env: prodteam: backend
kubectl apply -f app-tagged-es.yaml
验证
kubectl describe externalsecret app-tagged-es | sed -n '/Status/,$p'# 看 status.binding.name 与 events,没有 reason=ValidationFailed/SecretSyncedErrorkubectl get secret app-tagged -o jsonpath='{.data}' | jq 'keys | length'# 与 SSM 控制台按标签筛选结果数量一致
控制台对照:在 SSM 控制台的凭据管理系统页面按 Tag 过滤,比对返回数量。
清理
kubectl delete externalsecret app-tagged-es
常见错误
命中为 0:
标签尚未在 SSM 内部建立索引(
AttachResourcesTag 后通常 10–30s 生效)。等待后让 ESO 重新 reconcile(修改 ES 的 annotation 触发,或 kubectl rollout restart deploy/external-secrets -n kube-system)。凭据创建时未打 tag、且事后用错命令补打(控制台标签 ≠ SSM TagFilters 直查命中,建议优先使用
tccli ssm CreateSecret --Tags 一次性创建)。地域不一致(Tag 与 Secret 必须同 region)。
数量与控制台不符,原因是控制台默认显示全部地域 / 全部账号,请把控制台过滤条件切到 SecretStore 配置的
regionID。tccli ssm ListSecrets --TagFilters 报 InvalidParameterValue.TagsNotExisted,这是 tccli/SSM 直查接口偶发问题;不影响 ExternalSecret 同步(ESO 的 SDK 调用路径与 tccli 不同)。可改用 kubectl get secret <target> -o jsonpath='{.data}' \\| jq 'keys' 间接验证。6.5 案例 E 拉取指定版本:version
业务场景
金丝雀发布场景:SSM 同一个 Secret 同时存有
v1/v2 两个版本,新旧 Pod 分别锁定不同版本。前置条件
远端 Secret 已通过
tccli ssm PutSecretValue --SecretName dev-app-db --VersionId v2 --SecretString '...' 写入指定版本(首次创建新版本用 PutSecretValue;已有版本想改值用 UpdateSecret)。可用 tccli ssm ListSecretVersionIds --SecretName dev-app-db 确认。操作步骤
SSM 支持给同一个 Secret 写入多个版本号(例如走 CD 发布 v1、v2 两个版本供金丝雀)。要锁定某个版本:
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:name: app-db-v2-esnamespace: defaultspec:refreshInterval: 1hsecretStoreRef:name: tencent-ssm-storekind: SecretStoretarget:name: app-db-v2creationPolicy: Ownerdata:- secretKey: db-passwordremoteRef:key: dev-app-dbproperty: passwordversion: "v2" # 不写时 ESO 默认查 v_eso_latest,未命中再 fallback 到 SSM_Current;引用旁路用 tccli 创建(自定义 VersionId 如 v1/v2)的凭据时务必显式声明此字段,否则会报 "Secret does not exist"
kubectl apply -f app-db-v2-es.yaml
验证
# 1) ESO 已同步到指定版本kubectl get secret app-db-v2 -o jsonpath='{.data.db-password}' | base64 -d ; echo# 2) 与 SSM 中该版本的值比对tccli ssm GetSecretValue --SecretName dev-app-db --VersionId v2
清理
kubectl delete externalsecret app-db-v2-es
常见错误
VersionId not found,说明 SSM 中并未创建该 VersionId;可用 tccli ssm GetSecretVersionIds --SecretName dev-app-db 列出现有版本。远端更新了
v2 内容但本地未变,说明 ESO 命中缓存;等到 refreshInterval 后或手动 kubectl annotate externalsecret app-db-v2-es force-sync=$(date +%s) --overwrite。6.6 案例 F 二进制 Secret:decodingStrategy
业务场景
TLS 证书 PEM 文件(含换行)直接以 Base64 存入 SSM,应用 Pod 需要原始 PEM 内容挂到
tls.crt。前置条件
远端值是 Base64 编码后的 PEM 字节流。
操作步骤
SSM 中的二进制凭据(例如 PEM 证书直接以 base64 存)需要在 ESO 这边告诉它如何解码:
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:name: app-tls-esnamespace: defaultspec:refreshInterval: 1hsecretStoreRef:name: tencent-ssm-storekind: SecretStoretarget:name: app-tlscreationPolicy: Ownerdata:- secretKey: tls.crtremoteRef:key: dev-app-tlsdecodingStrategy: Base64 # None(默认) | Base64 | Base64URL | Auto
kubectl apply -f app-tls-es.yaml
验证
kubectl get secret app-tls -o jsonpath='{.data.tls\\.crt}' | base64 -d | openssl x509 -noout -subject# 期望能正确读出证书 Subject
清理
kubectl delete externalsecret app-tls-es
常见错误
openssl x509: unable to load certificate,说明 decodingStrategy 选错(远端是裸 PEM 时应选 None;远端是 Base64URL 编码时应选 Base64URL)。远端实际是 JSON 而非二进制,请改回 6.1/6.2 的
data / dataFrom.extract。6.7 案例 G 模板渲染:target.template
业务场景
应用只认
application.properties 这种文本配置文件,不接受逐 key 注入。希望把 SSM 拉到的字段在 ESO 端拼成完整 properties 文件,直接挂载即可。前置条件
同 6.1。模板里只能引用
data / dataFrom 已经导入的 key。操作步骤
场景一:把多个字段渲染成应用可读的
application.properties:apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: { name: app-cfg-es, namespace: default }spec:refreshInterval: 1hsecretStoreRef: { name: tencent-ssm-store, kind: SecretStore }target:name: app-cfgtemplate:engineVersion: v2type: Opaquedata:application.properties: |db.username={{ .username }}db.password={{ .password }}jdbc.url=jdbc:mysql://{{ .host }}:{{ .port }}/appdataFrom:- extract: { key: dev-app-db }
kubectl apply -f app-cfg-es.yaml
验证
kubectl get secret app-cfg -o jsonpath='{.data.application\\.properties}' | base64 -d# 期望渲染后的 properties 文本,{{ }} 已被实际值替换
清理
kubectl delete externalsecret app-cfg-es
常见错误
template: ...: undefined variable "$.xxx",说明模板里的占位符在 data / dataFrom 里不存在。渲染结果里出现字面量
{{ .username }},说明 engineVersion 没写或写成 v1,请显式 engineVersion: v2。6.8 案例 H — ClusterExternalSecret:批量分发到多个命名空间
业务场景
多个业务命名空间(如
team-a / team-b)共用同一份 DB 凭据,不希望每个 ns 各写一个 ExternalSecret。前置条件
每个目标 ns 都打上标签
eso-target=true(kubectl label ns team-a eso-target=true)。操作步骤
场景:同一份 SSM 凭据要复制到多个 namespace。
apiVersion: external-secrets.io/v1kind: ClusterExternalSecretmetadata:name: app-db-cesspec:externalSecretName: app-db # 在每个目标 ns 创建的 ES 名namespaceSelectors:- matchLabels:eso-target: "true"externalSecretSpec:refreshInterval: 1hsecretStoreRef: { name: tencent-ssm-cluster-store, kind: ClusterSecretStore }target: { name: app-db }dataFrom:- extract: { key: dev-app-db }
kubectl apply -f app-db-ces.yaml# 给目标 namespace 打标签kubectl label ns team-a eso-target=true --overwritekubectl label ns team-b eso-target=true --overwrite
验证
# 1) ClusterExternalSecret 自身就绪kubectl get clusterexternalsecret app-db-ces -o jsonpath='{.status.conditions}'# 2) 命中的 namespace 被记录kubectl get clusterexternalsecret app-db-ces -o jsonpath='{.status.provisionedNamespaces}'# 3) 每个命中 ns 都生成了同名 ExternalSecret 和目标 Secretfor ns in $(kubectl get ns -l eso-target=true -o name | awk -F/ '{print $2}'); doecho "== $ns =="kubectl -n "$ns" get externalsecret app-db -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' ; echokubectl -n "$ns" get secret app-db -o jsonpath='{.data}' | jq 'keys'done
清理
kubectl delete clusterexternalsecret app-db-ces# 各 ns 中由它生成的 ExternalSecret 会被自动清理;按 deletionPolicy 决定是否带走 Secret
常见错误
部分 ns 未生效,请确认这些 ns 标签
eso-target=true 是否到位;ESO 控制器对 ns 标签变化会异步重新协调。secretStoreRef 用了普通 SecretStore,跨 ns 必须用 ClusterSecretStore。6.9 案例 I 刷新与轮转:refreshInterval
业务场景
远端 SSM 改了
dev-app-db 的 password,希望 K8s Secret 在 1 分钟内自动跟进,避免应用读到旧密码。前置条件
同 6.1。
refreshInterval 越短,对 SSM API QPS 配额压力越大,建议生产环境 ≥ 30s。操作步骤
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:name: app-db-fast-esnamespace: defaultspec:refreshInterval: 1m # 默认 1h;最低 1s 但不建议低于 30ssecretStoreRef:name: tencent-ssm-storekind: SecretStoretarget:name: app-dbcreationPolicy: Ownerdata:- secretKey: DB_PASSremoteRef:key: dev-app-dbproperty: password
ESO 每到
refreshInterval 重新调一次 SSM GetSecretValue,发现远端值变化才会更新本地 Secret(resourceVersion 变化)。如需立即触发一次同步,给 ExternalSecret 打个 annotation:
kubectl annotate externalsecret <name> force-sync=$(date +%s) --overwrite。验证(轮转)
# 1) 远端改值(控制台或 CLI)。注意:UpdateSecret 必须显式指定 VersionId(默认主版本是 SSM_Current)tccli ssm UpdateSecret --SecretName dev-app-db --VersionId SSM_Current \\--SecretString '{"username":"admin","password":"NewP@ss"}'# 2) 等待一个 refreshInterval 周期,观察本地 Secret 的 resourceVersion 与值kubectl get secret app-db -o jsonpath='{.metadata.resourceVersion}' ; echokubectl get secret app-db -o jsonpath='{.data.DB_PASS}' | base64 -d ; echo# 期望:resourceVersion 增大;DB_PASS 变为 NewP@ss
清理
kubectl delete externalsecret app-db-fast-es
常见错误
远端已改但本地长时间不变,可能是 ESO 控制器卡死或限流;查
kubectl -n kube-system logs deploy/external-secrets --tail=200,必要时 force-sync annotation 强制刷新。RequestLimitExceeded,说明 refreshInterval 太短或 ExternalSecret 数量太多,调大间隔或在 SecretStore 设 ssm.rateLimitExceededMaxRetries。6.10 案例 J 删除策略:creationPolicy / deletionPolicy
业务场景
停用某个 ExternalSecret 时,希望连带把 K8s Secret 一并删除(避免脏数据);或反之,希望保留 K8s Secret 防止业务中断。
前置条件
同 6.1。
Merge 模式下目标 Secret 必须已存在。操作步骤
apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:name: app-db-policy-esnamespace: defaultspec:refreshInterval: 1hsecretStoreRef:name: tencent-ssm-storekind: SecretStoretarget:name: app-dbcreationPolicy: Owner # Owner(默认)|Orphan|Merge|NonedeletionPolicy: Retain # Retain(默认)|Delete|Mergedata:- secretKey: DB_PASSremoteRef:key: dev-app-dbproperty: password
Owner:ESO 创建并以 OwnerReference 持有。注意:删除 ExternalSecret 时 K8s 垃圾回收器会依据 OwnerReference 级联删除子资源,所以 Owner 下无论 deletionPolicy=Retain 还是 Delete 结果都是伴随 ES 消失;如需保留 Secret,请用 Orphan。Orphan:ESO 不设置 OwnerReference,是唯一能让 deletionPolicy=Retain 生效(删 ES 不带走 Secret)的选项。Merge:目标 Secret 必须已存在,ESO 仅合并字段。deletionPolicy: Delete:删除 ExternalSecret 时同步删除 K8s Secret;远端 SSM 不会被触碰(删远端用 PushSecret)。kubectl apply -f app-db-policy-es.yaml
验证(deletionPolicy)
kubectl delete externalsecret app-db-policy-eskubectl get secret app-db# 判断实验结果需结合 creationPolicy:# Owner+Retain : Secret 依然随 ES 消失(K8s GC 级联删除)# Owner+Delete : NotFound# Orphan+Retain : 仍在# Orphan+Delete : NotFound
清理
如已执行上一步删除 ExternalSecret,则无需额外清理;如需保留 ES、仅清理目标 Secret,按
creationPolicy=Owner 删 Secret 后 ES 会自动重建(除非也删除 ES)。常见错误
creationPolicy=Merge 但目标 Secret 不存在,ES 一直 Ready=False, reason=NotFound;先手工创建空 Secret 或改为 Owner。deletionPolicy=Retain 设了但删 ES 后 Secret 仍被带走,原因是底层 creationPolicy=Owner 设了 OwnerReference 导致 K8s GC 级联删除;要保留 Secret 必须同时设 creationPolicy=Orphan。误以为
deletionPolicy=Delete 会同步删 SSM 远端,实际不会。删远端要用 PushSecret 的 deletionPolicy: Delete(见 §7.5)。7. PushSecret 实操
本章每个案例都按统一骨架组织:业务场景、前置条件、操作步骤、验证、清理、常见错误。
7.1 案例 K 基础推送:把 K8s Secret 写入 SSM
业务场景
运维在 K8s 上生成了
app-secret(包含 db-password),希望把它反向写入腾讯云 SSM 以供集群之外的应用(例如 CVM、另一个集群)使用。前置条件
同 §6.1。
子账号/角色额外需要写权限(完整版见 §3.1.2):
操作步骤
# 1) 待推送的 K8s SecretapiVersion: v1kind: Secretmetadata:name: app-secretnamespace: defaulttype: OpaquestringData:db-password: "super-secret-password"---# 2) PushSecretapiVersion: external-secrets.io/v1alpha1kind: PushSecretmetadata:name: push-app-secretnamespace: defaultspec:refreshInterval: 1hsecretStoreRefs:- name: tencent-ssm-storekind: SecretStoreselector:secret:name: app-secretdata:- match:secretKey: db-password # 来自 K8s Secret 的 keyremoteRef:remoteKey: prod-app-db-password # SSM 中的 Secret 名
kubectl apply -f app-secret-and-push.yaml
验证
# 1) PushSecret Ready=Truekubectl get pushsecret push-app-secret -o jsonpath='{.status.conditions}'# 2) 远端值与本地源 Secret 一致# 说明:ESO PushSecret 写入腾讯 SSM 时,VersionId 固定为 v_eso_latest;# 默认走 SecretBinary(base64 编码),需在 shell 侧用 base64 -d 解出原值。# 如需写入纯字符串,可在 PushSecret 上加 metadata: secretPushFormat: string。LOCAL=$(kubectl get secret app-secret -o jsonpath='{.data.db-password}' | base64 -d)RESP=$(tccli ssm GetSecretValue --SecretName prod-app-db-password --VersionId v_eso_latest)STR=$(echo "$RESP" | jq -r '.SecretString')BIN=$(echo "$RESP" | jq -r '.SecretBinary')if [ -n "$STR" ] && [ "$STR" != "null" ]; then REMOTE="$STR"; else REMOTE=$(echo "$BIN" | base64 -d); fi[ "$LOCAL" = "$REMOTE" ] && echo OK || echo MISMATCH
控制台校验:在 SSM 控制台的凭据管理系统页面,应能看到名为
prod-app-db-password 的凭据,描述包含 managed-by:external-secrets,查看凭据值与本地 K8s Secret 完全一致。清理
kubectl delete pushsecret push-app-secret # 默认 deletionPolicy=None,远端保留kubectl delete secret app-secret # 可选:删本地源kubectl annotate secret prod-cleanup --field-manager=manual --overwrite=false 2>/dev/null || true# 不需要远端时手动在控制台/CLI 删除 prod-app-db-password(或参考 7.5 联动删除)
常见错误
Forbidden ssm:CreateSecret,说明缺少 §3.1.2 的写权限。secret already exists and not managed by ESO,说明同名凭据已存在且描述不含 ESO managed-by;SSM 控制台手动加上 managed-by:external-secrets 描述后重试,或换个 remoteKey。控制台查不到远端凭据,可能是 SecretStore 配了
prefix,实际 SecretName 是 prefix + remoteKey,请带前缀搜。7.2 案例 L 设置 Tags / Description / KMSKeyID
业务场景
推送到 SSM 的凭据需要业务分类与企业 CMK 加密,例如打上
env=production / team=backend 标签与财务分摊依据,并使用业务专用 CMK。前置条件
同 7.1;如需打 Tag,子账号额外需要
tag:ModifyResourceTags(§3.1.2 末段)。kmsKeyId 必须在同地域且调用者有 kms:Encrypt/Decrypt 权限。操作步骤
apiVersion: external-secrets.io/v1alpha1kind: PushSecretmetadata:name: push-with-metaspec:secretStoreRefs:- { name: tencent-ssm-store, kind: SecretStore }selector:secret: { name: app-secret }data:- match:secretKey: db-passwordremoteRef:remoteKey: prod-app-db-passwordmetadata:apiVersion: kubernetes.external-secrets.io/v1alpha1kind: PushSecretMetadataspec:description: "Database credentials for prod"tags:env: productionteam: backendkmsKeyId: kms-xxxxxxxx # 可选;不填则使用 SSM 平台默认 CMKsecretPushFormat: string # string 或 binary,默认 binary
kubectl apply -f push-with-meta.yaml
注意:
1. Tags 仅在创建时生效(SSM 没有独立 TagResource API),后期修改 spec.tags 不会反向同步;需要改 Tag 请在控制台人工调整。
2. 禁止在
tags 中手动写入键 managed-by。验证
kubectl get pushsecret push-with-meta -o jsonpath='{.status.conditions}'# 远端属性核对tccli ssm DescribeSecret --SecretName prod-app-db-password \\| jq '{Description, KmsKeyId}'# 标签查询走 tag 服务(不是 ssm 子命令);--ResourceIds 必须是 JSON 数组tccli tag DescribeResourceTagsByResourceIds \\--ServiceType ssm --ResourcePrefix secret --ResourceRegion ap-guangzhou \\--ResourceIds '["prod-app-db-password"]' \\| jq '.Tags[] | {TagKey, TagValue}'# 期望:Description 与 spec 一致;Tags 含 env=production、team=backend、managed-by=external-secrets# 若 secretPushFormat=string,GetSecretValue 的 SecretString 非空、SecretBinary 为空;# 若 secretPushFormat=binary(默认),SecretBinary 是 base64 编码值,需 base64 -d 还原。
清理
kubectl delete pushsecret push-with-meta
常见错误
tags must not contain reserved key managed-by,请勿手动写 managed-by。kms key not found / not authorized,说明 KMS CMK 与 SecretStore 不同地域,或 ESO 权限不足。Tag 在控制台未出现,原因是
prod-app-db-password 是被老 PushSecret 创建过的,此后改 spec.tags 不会补发。删除远端后重推。7.3 案例 M Property 级局部更新(JSON 合并)
业务场景
远端
prod-app-config 是多字段 JSON(包含 db.host/db.port/db.password),多个负责人分别运维,仅希望每个 PushSecret 只覆盖自己负责的子路径,不要影响其他字段。前置条件
同 7.1。
远端原始值是合法 JSON(如果远端未创建或不是 JSON,ESO 会自动创建为空 JSON 并写入该路径)。
操作步骤
把 K8s Secret 的某个值合并进远端 JSON Secret 的指定路径,保留同 JSON 中其他字段。
假设远端
prod-app-config 当前值是:{ "db": { "host": "10.0.0.1", "port": 3306 } }
说明:
远端尚未创建?先用 tccli 一次性建好基础 JSON(VersionId 用 ESO 默认的
v_eso_latest,描述含 managed-by:external-secrets 以便后续被 ESO 接管):tccli ssm CreateSecret --SecretName prod-app-config --VersionId v_eso_latest \\--SecretString '{"db":{"host":"10.0.0.1","port":3306}}' \\--Description "secret 'managed-by:external-secrets'"
apiVersion: external-secrets.io/v1alpha1kind: PushSecretmetadata:name: push-propertyspec:refreshInterval: 30ssecretStoreRefs:- { name: tencent-ssm-store, kind: SecretStore }selector:secret: { name: app-secret } # 假设其中有 key: db-passworddata:- match:secretKey: db-passwordremoteRef:remoteKey: prod-app-configproperty: db.password # 只覆盖 JSON 路径 db.password
合并后远端变为:
{ "db": { "host": "10.0.0.1", "port": 3306, "password": "super-secret-password" } }
行为速查:
场景 | 行为 |
无 property | 全量覆盖远端 |
有 property 且远端不存在 | 创建空 JSON 并写入该路径 |
有 property 且远端存在 | 读取后经 sjson 合并再写回 |
推送值与远端相同 | 跳过写入,避免无意义 API 调用 |
kubectl apply -f push-property.yaml
验证
kubectl get pushsecret push-property -o jsonpath='{.status.conditions}'# ESO 默认以 SecretBinary(base64 编码)写回;兼容两种格式解出 JSONRESP=$(tccli ssm GetSecretValue --SecretName prod-app-config --VersionId v_eso_latest)STR=$(echo "$RESP" | jq -r '.SecretString')BIN=$(echo "$RESP" | jq -r '.SecretBinary')if [ -n "$STR" ] && [ "$STR" != "null" ]; then VAL="$STR"; else VAL=$(echo "$BIN" | base64 -d); fiecho "$VAL" | jq .# 期望输出:{"db":{"host":"10.0.0.1","port":3306,"password":"super-secret-password"}}# 即原 host/port 字段被保留,仅 password 被合并写入
清理
kubectl delete pushsecret push-property# 需要同步抹除该路径?可手动下一份不含该 key 的 K8s Secret + property 推送覆写,或控制台修改
常见错误
failed to merge json: invalid path,说明 property 路径中含未转义点号,sjson 方言下点号是层级分隔符;如需包含字面点,请在路径里转义 db\\.password。两个 PushSecret 同时写同一远端不同 property,可能产生
version conflict,默认会在下轮调谐重试;可错峰调两者的 refreshInterval。7.4 案例 N K8s Secret 滚动后自动同步到 SSM
业务场景
运维脚本定期轮换 K8s Secret(例如 cert-manager 轮换 TLS、手动
kubectl create secret ... --dry-run | apply),希望轮换后远端 SSM 能在 30s 内同步。前置条件
同 7.1。
操作步骤
PushSecret 默认不监听源 Secret 变化,只在自身 spec 变更或
refreshInterval 到期时同步。鼓励显式设置:spec:refreshInterval: 30s
将
refreshInterval 设置得越短,rotate 越及时,但也会增加 SSM API 调用频率与配额消耗。验证(rotate 行为)
# 1) 滚动源 Secretkubectl create secret generic app-secret --from-literal=db-password=NewP@ss \\--dry-run=client -o yaml | kubectl apply -f -# 2) 等待一个 refreshInterval(示例 30s)sleep 35# 3) 远端值跟随更新(默认 SecretBinary,需 base64 -d;secretPushFormat=string 时直接读 SecretString)RESP=$(tccli ssm GetSecretValue --SecretName prod-app-db-password --VersionId v_eso_latest)STR=$(echo "$RESP" | jq -r '.SecretString')BIN=$(echo "$RESP" | jq -r '.SecretBinary')if [ -n "$STR" ] && [ "$STR" != "null" ]; then echo "$STR"; else echo "$BIN" | base64 -d; fi# 期望输出:NewP@ss
清理
kubectl delete pushsecret push-app-secret
常见错误
远端未随时更新,说明
refreshInterval 没设或过长;可临时下 kubectl annotate pushsecret push-app-secret force-sync=$(date +%s) --overwrite 验证。远端快照中出现多个版本,说明 ESO PushSecret 默认只写入并维护
v_eso_latest 这一固定 VersionId,不会因频繁 rotate 在远端堆出多个版本;如需多版本(例如旁路写入 v0/v1 用于灰度)请直接走 tccli PutSecretValue 自行管理。7.5 案例 O deletionPolicy=Delete:连带删除远端
业务场景
临时业务下线,不希望远端 SSM 上遗留废弃凭据,期望删除 PushSecret 同时带走远端(仍会调用 SSM
DeleteSecret,受恢复窗口保护)。前置条件
额外写权限
ssm:DeleteSecret。SecretStore 上
ssm.recoveryWindowInDays 决定删除后还能不能恢复(0 立即销毁)。操作步骤
apiVersion: external-secrets.io/v1alpha1kind: PushSecretmetadata:name: push-app-secretnamespace: defaultspec:refreshInterval: 1hdeletionPolicy: Delete # 删 PushSecret 时同步删远端secretStoreRefs:- name: tencent-ssm-storekind: SecretStoreselector:secret:name: app-secretdata:- match:secretKey: db-passwordremoteRef:remoteKey: prod-app-db-password
注意点:
删除等同于调用
DeleteSecret,恢复窗口由 SecretStore 的 ssm.recoveryWindowInDays 决定;未配置时默认 0,即立即销毁,远端会直接 ResourceNotFound,没有 PendingDelete 中间态。如需软删后可恢复,请在 SecretStore 上显式设置 provider.tencent.ssm.recoveryWindowInDays(取值 7-30)。ESO 会校验 Description 是否包含 ESO managed-by 标识,否则拒绝删除(详见 7.6)。
kubectl apply -f push-app-secret-delete.yaml
验证(连带删除)
kubectl delete pushsecret push-app-secret# 等待协调一次(约 5-15s)sleep 15# 远端状态:recoveryWindowInDays>0 时为 PendingDelete;=0 时直接消失tccli ssm DescribeSecret --SecretName prod-app-db-password \\| jq '{Status, DeleteTime}'# 期望:Status=PendingDelete 或调用返回 ResourceNotFound
清理
# 如需从 PendingDelete 状态召回:tccli ssm RestoreSecret --SecretName prod-app-db-password# 如需立即销毁:进 SSM 控制台点立即删除或使用同名 API
常见错误
删 PushSecret 但远端仍在
Enabled,见 7.6,远端不是 ESO 创建的,被防误删拦截。远端状态是
PendingDelete 但控制台看不到,原因是控制台默认过滤了待删除项,切全部状态。7.6 案例 P 防误删:非 ESO 创建的 Secret 不会被删除
业务场景
某个远端凭据
prod-app-manual 是运维人员事先手工在 SSM 控制台创建的(描述未含 ESO managed-by),即使用 PushSecret 接管后 deletionPolicy: Delete,运维仍希望远端不被误删。前置条件
远端 Description 中不包含
managed-by:external-secrets。操作步骤
无需额外 yaml;7.5 中的 PushSecret 例子在此场景下会默认启用保护。
如果远端 Secret 是手动通过 SSM 控制台创建的(描述里没有
managed-by:external-secrets),即使设置 deletionPolicy: Delete,ESO 删除 PushSecret 后不会触碰远端 —— 它在 Enabled 状态保持不变。这一保护机制无需额外配置。实操准备(一次性):
# 1) 模拟"运维手工创建的远端",描述里不含 managed-bytccli ssm CreateSecret --SecretName prod-app-manual --VersionId v0 \\--SecretString 'manual-value' --Description 'manually created by ops'# 2) 创建 PushSecret 接管该远端,并显式 deletionPolicy: Deletecat <<'EOF' | kubectl apply -f -apiVersion: external-secrets.io/v1alpha1kind: PushSecretmetadata:name: push-manualnamespace: defaultspec:refreshInterval: 1hdeletionPolicy: DeletesecretStoreRefs:- name: tencent-ssm-storekind: SecretStoreselector:secret:name: app-secretdata:- match:secretKey: db-passwordremoteRef:remoteKey: prod-app-manualEOF
说明:
因为 ESO 写入固定 VersionId=
v_eso_latest,而手动创建的远端版本是 v0,PushSecret 的同步阶段可能报 ResourceNotFound: can not find secrets(这是 ESO 自身保护,它不会把数据强行写入它不认识的版本流上)。这与“删除时不动远端”是两条独立的保护。验证(保护机制)
# 前提:prod-app-manual 是控制台手动创建的,描述不含 managed-bykubectl delete pushsecret push-manualsleep 15tccli ssm DescribeSecret --SecretName prod-app-manual | jq '{Description, Status}'# 期望:Description 仍是手动设置的内容(未被 ESO 覆盖),Status=Enabled(远端未被删除)
清理
# 远端确认无用后再手动清理tccli ssm DisableSecret --SecretName prod-app-manualtccli ssm DeleteSecret --SecretName prod-app-manual --RecoveryWindowInDays 0
7.7 案例 Q 资源策略 Resource Policy(PolicyDocument 内联)
业务场景
远端 SSM 凭据需要授权另一个子账号(uin/200000000002)只读;希望资源策略跟 PushSecret 一起纳入 GitOps,避免控制台手动改。
前置条件:同 7.1;额外需要 §3.1.3 中的 CAM 策略管理权限(
cam:CreatePolicy / cam:GetPolicy / cam:UpdatePolicy / cam:DeletePolicy / cam:ListPolicies)。操作步骤
把腾讯云 CAM 资源策略直接写在 PushSecret 中:
apiVersion: external-secrets.io/v1alpha1kind: PushSecretmetadata:name: push-with-policynamespace: defaultspec:refreshInterval: 1hsecretStoreRefs:- name: tencent-ssm-storekind: SecretStoreselector:secret:name: app-secretdata:- match:secretKey: db-passwordremoteRef:remoteKey: prod-app-db-passwordmetadata:apiVersion: kubernetes.external-secrets.io/v1alpha1kind: PushSecretMetadataspec:resourcePolicy:policyDocument: |{"version": "2.0","statement": [{"effect": "allow","principal": { "qcs": ["qcs::cam::uin/100000000001:uin/200000000002"] },"action": ["ssm:GetSecretValue"],"resource": ["*"]}]}
ESO 会把
policyDocument 翻译成一份 CAM 自定义策略(命名规则:eso-ssm-<remoteKey>),调用 cam:CreatePolicy / cam:UpdatePolicy 写入。说明:
生命周期说明:CAM 策略只在 PushSecret 同步路径 上被管理。如果你从 PushSecret 中移除
metadata.spec.resourcePolicy 并 apply,ESO 会自动调 cam:DeletePolicy 清理;但直接 kubectl delete pushsecret 不会触发该清理(即使 deletionPolicy: Delete 也只会删 SSM 凭据本身,不动 CAM 策略)。推荐的清理顺序见下面“清理”节。kubectl apply -f push-with-policy.yaml
验证(策略已下发)
kubectl get pushsecret push-with-policy -o jsonpath='{.status.conditions}'# 列出 ESO 托管的 CAM 自定义策略;命名规则:eso-ssm-<remoteKey>tccli cam ListPolicies --Keyword eso-ssm-prod-app-db-password --Scope Local --Page 1 --Rp 5 \\| jq '.List[] | {PolicyId, PolicyName}'# 期望:能看到一条 PolicyName="eso-ssm-prod-app-db-password" 的 Custom 策略# 进一步核对策略文档内容PID=$(tccli cam ListPolicies --Keyword eso-ssm-prod-app-db-password --Scope Local --Page 1 --Rp 5 \\| jq '.List[0].PolicyId')tccli cam GetPolicy --PolicyId "$PID" | jq '{PolicyName, PolicyDocument}'# 期望:PolicyDocument JSON 与 spec 中的 policyDocument 等价(statement.principal.qcs / action / resource 一致)
清理
# 推荐两步清理:先从 spec 中去掉 resourcePolicy 、apply(触发 ESO 调 cam:DeletePolicy),# 再删除 PushSecret。否则 CAM 策略会被遗留,需手动走下面的后备命令。# 后备:手动清理遗留的 CAM 自定义策略PID=$(tccli cam ListPolicies --Keyword eso-ssm-prod-app-db-password --Scope Local --Page 1 --Rp 5 \\| jq '.List[0].PolicyId')[ -n "$PID" ] && [ "$PID" != "null" ] && tccli cam DeletePolicy --PolicyId "[$PID]"kubectl delete pushsecret push-with-policy
常见错误
policy document parse error,说明 JSON 语法错、多余逗号。Forbidden cam:CreatePolicy / Forbidden cam:UpdatePolicy,说明缺少 §3.1.3 权限。策略似乎未生效,请到 SSM 控制台凭据策略页查看是否写入;也可能调用者与资源同账号,被账号级权限覆盖。
7.8 案例 R 资源策略 Resource Policy(PolicySourceRef 引用 ConfigMap)
业务场景
资源策略在 GitOps 仓库独立维护,多个 PushSecret 复用同一份 JSON,不希望复制粘贴到每个 PushSecret 中。
前置条件
同 7.7;额外需要一份装载策略 JSON 的 ConfigMap 或 Secret。
操作步骤
把策略文档放在 ConfigMap / Secret 中,PushSecret 通过引用使用:
apiVersion: v1kind: ConfigMapmetadata:name: app-policynamespace: defaultdata:policy.json: |{"version":"2.0","statement":[{"effect":"allow","principal":{"qcs":["qcs::cam::uin/100000000001:uin/200000000002"]},"action":["ssm:GetSecretValue"],"resource":["*"]}]}---apiVersion: external-secrets.io/v1alpha1kind: PushSecretmetadata:name: push-with-policy-refspec:secretStoreRefs:- { name: tencent-ssm-store, kind: SecretStore }selector:secret: { name: app-secret }data:- match:secretKey: db-passwordremoteRef:remoteKey: prod-app-db-passwordmetadata:apiVersion: kubernetes.external-secrets.io/v1alpha1kind: PushSecretMetadataspec:resourcePolicy:policySourceRef:kind: ConfigMap # 或 Secretname: app-policykey: policy.json# namespace 可省略,省略时默认 SecretStore 所在命名空间
说明:
policyDocument 与 policySourceRef 互斥,二选一。kubectl apply -f push-with-policy-ref.yaml
验证
kubectl get pushsecret push-with-policy-ref -o jsonpath='{.status.conditions}'# 列出 ESO 托管的 CAM 自定义策略PID=$(tccli cam ListPolicies --Keyword eso-ssm-prod-app-db-password --Scope Local --Page 1 --Rp 5 \\| jq '.List[0].PolicyId')tccli cam GetPolicy --PolicyId "$PID" | jq '{PolicyName, PolicyDocument}'# 期望:PolicyName="eso-ssm-prod-app-db-password",PolicyDocument 与 ConfigMap app-policy.policy.json 等价
修改 ConfigMap 后,PushSecret 默认按
refreshInterval 重新下发;想立刻生效可执行 kubectl annotate pushsecret push-with-policy-ref force-sync=$(date +%s) --overwrite。清理
# 同 7.7:先去掉 resourcePolicy 字段并 apply(触发 ESO cam:DeletePolicy),再删 PushSecret + ConfigMap。# 后备:手动清理遗留的 CAM 自定义策略PID=$(tccli cam ListPolicies --Keyword eso-ssm-prod-app-db-password --Scope Local --Page 1 --Rp 5 \\| jq '.List[0].PolicyId')[ -n "$PID" ] && [ "$PID" != "null" ] && tccli cam DeletePolicy --PolicyId "[$PID]"kubectl delete pushsecret push-with-policy-refkubectl delete configmap app-policy
常见错误
referenced ConfigMap not found,说明名称或 namespace 不对。key 'policy.json' not found in ConfigMap,说明 ConfigMap 里该 key 不存在。使用 Secret 类型 policySourceRef 报权限错,说明 ESO ServiceAccount 需能
get 对应 namespace 下的该 Secret。7.9 PushSecretMetadata 字段速查
metadata:apiVersion: kubernetes.external-secrets.io/v1alpha1kind: PushSecretMetadataspec:tags: { env: production, team: backend } # 仅创建时生效description: "..." # 默认 secret 'managed-by:external-secrets'kmsKeyId: kms-xxxxxxxx # 可选secretPushFormat: binary # binary(默认) 或 stringresourcePolicy:policyDocument: |{"version":"2.0","statement":[...]}# 或者:policySourceRef:kind: ConfigMap # ConfigMap 或 Secretname: app-policykey: policy.jsonnamespace: "" # 可省略
8. Generators 实操
本章每个案例都按统一骨架组织:业务场景、前置条件、操作步骤、验证、清理、常见错误。
STS / TCR Generator endpoint 已修复并实测通过(镜像 ≥
v3.0.3)历史问题:Generator 未读取 endpoint 环境变量,固定走公网域名,在仅放通 VPC 内网域名(
*.internal.tencentcloudapi.com)的集群里出现 dial tcp ... i/o timeout。已修复:
generators/v1/tencentsts/tencentsts.go::stsFactory 走 getSTSEndpoint() 读 TENCENT_STS_ENDPOINTgenerators/v1/tencenttcr/tencenttcr.go::defaultTCRFactory 走 getTCREndpoint() 读 TENCENT_TCR_ENDPOINTproviders/v1/tencent/util/provider.go 增 DefaultTCREndpoint / GetTCREndpoint()apis/externalsecrets/v1、v1beta1 的 generatorRef.kind enum 补 TencentTCRAccessTokenHelm chart
rbac.yaml 的 ClusterRole 补 tencenttcraccesstokens list/watch/get8.1 案例 S TencentSTSSessionToken:动态颁发腾讯云临时凭证
业务场景
集群内工作负载需要访问腾讯云 SSM,但不希望长期 AKSK 落在集群里;期望 ESO 定期去 STS 换临时凭证并物化为 K8s Secret。
前置条件
已按 §3.2.2 创建被扮演角色(
eso-runtime-role),信任关系允许调用者子账号 AssumeRole。调用者(本例的长期 Secret 对应者)拥有
sts:AssumeRole 权限(§3.2.2 步骤 3)。操作步骤
# 长期身份 Secret —— 用来调 STS 换临时凭证apiVersion: v1kind: Secretmetadata:name: tencent-long-termnamespace: defaulttype: OpaquestringData:secret-id: "AKIDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"secret-key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"---# GeneratorapiVersion: generators.external-secrets.io/v1alpha1kind: TencentSTSSessionTokenmetadata:name: tencent-sts-gennamespace: defaultspec:region: ap-guangzhouauth:secretRef:accessKeyIDSecretRef:name: tencent-long-termkey: secret-idaccessKeySecretSecretRef:name: tencent-long-termkey: secret-keyrequestParameters:roleArn: "qcs::cam::uin/100000000001:roleName/eso-runtime-role"roleSessionName: "eso-runtime-session"---# 让 ESO 把 Generator 输出物化到一个 K8s SecretapiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:name: tencent-sts-creds-esnamespace: defaultspec:refreshInterval: 30m # 每 30 分钟换一次临时凭证target:name: tencent-sts-creds # 产出 Secret 名dataFrom:- sourceRef:generatorRef:apiVersion: generators.external-secrets.io/v1alpha1kind: TencentSTSSessionTokenname: tencent-sts-gen
kubectl apply -f tencent-sts.yaml
tencent-sts-creds Secret 中会有 4 个键:key | 含义 |
access_key_id | 临时 SecretId |
secret_access_key | 临时 SecretKey |
session_token | 临时 Token(必须与上面两个一起使用) |
expiration | 过期时间,Unix 秒 |
验证
# 1) ExternalSecret Ready=Truekubectl get externalsecret tencent-sts-creds-es -o jsonpath='{.status.conditions}'# 2) 产出 Secret 四个键齐全kubectl get secret tencent-sts-creds -o jsonpath='{.data}' | jq 'keys'# 期望:["access_key_id","expiration","secret_access_key","session_token"]# 3) expiration 为未来时间戳(默认 7200s 后)EXP=$(kubectl get secret tencent-sts-creds -o jsonpath='{.data.expiration}' | base64 -d)echo "now=$(date +%s) expiration=$EXP delta=$((EXP - $(date +%s)))s"# 期望:delta > 0# 4) 用临时凭证发起一次真实调用,确认权限可用export TENCENTCLOUD_SECRET_ID=$(kubectl get secret tencent-sts-creds -o jsonpath='{.data.access_key_id}' | base64 -d)export TENCENTCLOUD_SECRET_KEY=$(kubectl get secret tencent-sts-creds -o jsonpath='{.data.secret_access_key}' | base64 -d)export TENCENTCLOUD_TOKEN=$(kubectl get secret tencent-sts-creds -o jsonpath='{.data.session_token}' | base64 -d)tccli ssm ListSecrets --region ap-guangzhou# 期望:能成功列出(按 §3.2.2 的角色权限)
清理
kubectl delete externalsecret tencent-sts-creds-eskubectl delete tencentstssessiontoken tencent-sts-genkubectl delete secret tencent-long-term tencent-sts-creds --ignore-not-found
常见错误
unable to get Tencent STS credentials / AssumeRole 报 401,说明被扮演角色信任关系不让当前子账号扮演,或子账号缺 sts:AssumeRole。roleSessionName 报格式错,要求长度 2-128,仅字母数字与 =,.@_-。输出 Secret 中缺
session_token,说明用了错的 Generator 类型。8.2 案例 T 用临时凭证驱动 SecretStore 闭环
业务场景
期望集群内全局不依赖长期 AKSK 访问 SSM:用 8.1 产出的临时凭证作为另一个 SecretStore 的认证源,后续业务 ExternalSecret/PushSecret 都走临时凭证。
前置条件
8.1 完成,输出 Secret
tencent-sts-creds 可用。操作步骤
把上一步产出的
tencent-sts-creds 作为另一个 SecretStore 的认证源,从而全集群无长期 AKSK:apiVersion: external-secrets.io/v1kind: SecretStoremetadata:name: tencent-ssm-sts-storenamespace: defaultspec:provider:tencent:regionID: ap-guangzhouauth:secretRef:accessKeyIDSecretRef:name: tencent-sts-credskey: access_key_idaccessKeySecretSecretRef:name: tencent-sts-credskey: secret_access_keysessionTokenSecretRef: # ← 关键:使用临时 Tokenname: tencent-sts-credskey: session_token
之后任何
ExternalSecret / PushSecret 引用 tencent-ssm-sts-store 都用临时凭证签名访问 SSM。kubectl apply -f sts-store.yaml
验证(闭环)
# 1) 新 SecretStore 通过临时凭证就绪kubectl get secretstore tencent-ssm-sts-store -o jsonpath='{.status.conditions}'# 2) 用它跑一个 ExternalSecretcat <<EOF | kubectl apply -f -apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata: { name: sts-probe-es, namespace: default }spec:refreshInterval: 1msecretStoreRef: { name: tencent-ssm-sts-store, kind: SecretStore }target: { name: sts-probe }data:- secretKey: proberemoteRef: { key: dev-app-db, property: username }EOFkubectl get externalsecret sts-probe-es -o jsonpath='{.status.conditions}'kubectl get secret sts-probe -o jsonpath='{.data.probe}' | base64 -d ; echo
清理
kubectl delete externalsecret sts-probe-eskubectl delete secretstore tencent-ssm-sts-store
常见错误
SecretStore
Ready=False reason=AuthFailure,说明临时凭证过期、轮转未及时(参考 8.3 调低 refreshInterval)。signature does not match,说明 sessionTokenSecretRef 未引用或 key 不是 session_token。8.3 案例 U 临时凭证自动轮转
业务场景
临时凭证默认 7200s 后过期,需要在过期前自动轮转。推荐
refreshInterval < durationSeconds 的一半。前置条件
同 8.1。
refreshInterval 越短调用 STS API 接口频率会越高。操作步骤
ExternalSecret.spec.refreshInterval 决定 Generator 重调 STS 的频率:spec:refreshInterval: 30m # 推荐:< durationSeconds 的一半
行为:到期时 ESO 重新调用
AssumeRole,用新返回的临时凭证覆盖目标 Secret,下游 SecretStore 自动感知(控制器会 watch Secret)。验证(轮转发生)
# 周期性观察 Secret 的 resourceVersion 与 expirationfor i in 1 2 3; doRV=$(kubectl get secret tencent-sts-creds -o jsonpath='{.metadata.resourceVersion}')EXP=$(kubectl get secret tencent-sts-creds -o jsonpath='{.data.expiration}' | base64 -d)echo "loop=$i rv=$RV expiration=$EXP"sleep $((30*60)) # 与 refreshInterval 一致done# 期望:每轮 rv 都增大、expiration 推进 ≥ refreshInterval
清理
本节只调参,不需额外资源;如不再需要轮转可将
refreshInterval 调回默认值。常见错误
expiration 不推进,说明
refreshInterval 太长或控制器限流;查 kubectl -n kube-system logs deploy/external-secrets。下游 SecretStore 不同步轮转,请检查指向的 Secret 是否同一份;ESO 是否启用了 watch Secret(默认启用)。
8.4 案例 V 自定义 durationSeconds / policy / externalId
业务场景
默认临时凭证能力过宽(同角色全部权限)。希望在扮演时在角色上叠加 inline policy 进一步收窄:例如只准读 SSM,不准删;跨账号扮演时需要 externalId。
前置条件
同 8.1。跨账号扮演时,被扮演角色的信任关系需明文要求 externalId。
操作步骤
spec:region: ap-guangzhouauth: { ... }requestParameters:roleArn: "qcs::cam::uin/100000000001:roleName/eso-runtime-role"roleSessionName: "eso-runtime-session"durationSeconds: 3600 # 范围 900-43200,默认 7200policy: | # 进一步缩小临时凭证权限范围{"version":"2.0","statement":[{"effect":"allow","action":["ssm:GetSecretValue"],"resource":["*"]}]}externalId: "ext-2026" # 跨账号扮演时使用
kubectl apply -f sts-narrow.yaml
验证(policy 已收窄)
# 用上面方法把临时凭证导出环境变量后tccli ssm GetSecretValue --region ap-guangzhou --SecretName dev-app-db --VersionId v0 # 应成功tccli ssm DeleteSecret --region ap-guangzhou --SecretName dev-app-db 2>&1 | tail -3 # 应被拒绝(UnauthorizedOperation;policy 仅放通 GetSecretValue)# tccli 3.x 把 SDK 异常拼在 usage 提示之后,请用 `tail -3` 而不是 `head -3`,否则只看到 usage 看不到 `code:UnauthorizedOperation` 关键行。
清理
# 恢复默认设置:删除该 Generator 或去掉 policy/externalId/durationSeconds 三个可选字段后重新 apply
常见错误
policy parse error,说明 inline policy JSON 不合法。external id required / mismatch,说明被扮演角色信任关系要求 externalId 但你未提供或不匹配。durationSeconds out of range,有效范围为 900-43200,且不得超过角色 max session duration。8.5 案例 W ClusterGenerator:跨命名空间共享一个 Generator
业务场景
多业务 ns(
team-a / team-b……)都需要同一份临时凭证,不希望每个 ns 都复制一份 Generator 资源。说明:
重要限制(与 ESO 上游设计一致):ClusterGenerator 是集群级资源,但其
secretRef 引用的长期凭证 Secret 仍必须存在于"消费它的 ExternalSecret 所在 ns"。社区上游 Generator(ECRAuthorizationToken、Grafana、TencentSTS、TencentTCR 等)都遵循同一安全约定:Generator 一律不允许跨 ns 拿 Secret,避免一个 cluster-scoped 对象绕过 ns 隔离去读取任意 ns 下的凭证。跨 ns 集中管理长期凭证是
ClusterSecretStore 的设计目标,不是 ClusterGenerator 的。因此:
ClusterGenerator 解决的是"Generator 配置复用"(避免在每个 ns 重复声明 region/roleArn/auth 字段结构);
它不能解决"长期 AKSK Secret 集中存放在 default、所有 ns 都跨 ns 引用"——即使 yaml 里写了
secretRef.namespace,控制器也会忽略该字段、强制回到 ExternalSecret 所在的 ns 查找;推荐 workaround:在每个目标 ns 预放一份同名长期 Secret(可通过 GitOps、kubed/secret-replicator、或额外用 ESO 自身从一个中心 SecretStore 同步出来),yaml 中省略
secretRef.namespace 字段。前置条件
每个目标 ns(
team-a、team-b ……)都需要事先存在一份同名的长期凭证 Secret(如 tencent-long-term)。操作步骤
集群级资源,配合
ExternalSecret.dataFrom.sourceRef.generatorRef.kind: ClusterGenerator 使用。注意 yaml 里 secretRef 不写 namespace——它会被忽略,控制器始终在 ExternalSecret 所在 ns 查找 tencent-long-term:apiVersion: generators.external-secrets.io/v1alpha1kind: ClusterGeneratormetadata:name: tencent-sts-cluster-genspec:kind: TencentSTSSessionTokengenerator:tencentSTSSessionTokenSpec:region: ap-guangzhouauth:secretRef:accessKeyIDSecretRef:name: tencent-long-term # ← 必须存在于消费它的 ExternalSecret 所在 nskey: secret-idaccessKeySecretSecretRef:name: tencent-long-termkey: secret-keyrequestParameters:roleArn: "qcs::cam::uin/100000000001:roleName/eso-runtime-role"roleSessionName: "eso-cluster-session"---apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:name: any-ns-stsnamespace: team-a # 该 ns 下需预先存在 tencent-long-termspec:refreshInterval: 30mtarget: { name: tencent-sts-creds }dataFrom:- sourceRef:generatorRef:apiVersion: generators.external-secrets.io/v1alpha1kind: ClusterGeneratorname: tencent-sts-cluster-gen
# 1) 每个目标 ns 落一份同名长期 Secret(示例直接从 default 复制)for NS in team-a team-b; dokubectl create ns "$NS" --dry-run=client -o yaml | kubectl apply -f -kubectl -n default get secret tencent-long-term -o yaml \\| sed "s/namespace: default/namespace: $NS/" \\| kubectl apply -f -done# 2) 应用 ClusterGenerator + ExternalSecretkubectl apply -f cluster-gen.yaml
验证
kubectl -n team-a get externalsecret any-ns-sts -o jsonpath='{.status.conditions}'kubectl -n team-a get secret tencent-sts-creds -o jsonpath='{.data}' | jq 'keys'# 期望:四个键齐全;其它命名空间使用同一个 ClusterGenerator 也能产出(前提:该 ns 下也有同名长期 Secret)
清理
kubectl -n team-a delete externalsecret any-ns-stskubectl delete clustergenerator tencent-sts-cluster-gen# 长期凭证 Secret 视情况保留
常见错误
cannot get Kubernetes secret "tencent-long-term" from namespace "team-a": secrets ... not found,说明该 ns 下没有同名长期 Secret,按本节"前置条件"复制一份过去;不要指望 yaml 里的 secretRef.namespace 字段会跨 ns 解析(参见上方 重要限制)。team-a ns 下看不到产出的 Secret,说明该 ns 未创建或拼写错。想避免每 ns 复制 Secret?可改用 ClusterSecretStore(集中读 SSM)+ ExternalSecret 模式,或在集群里跑一个 Secret 复制器(kubed / external-secrets refreshable target / OpenShift
Secret reflector)。8.6 案例 X TencentTCRAccessToken:颁发 TCR 镜像仓库访问令牌
业务场景
TKE 工作负载需要从腾讯云 TCR 拉镜像,但不希望手工
docker login、手工轮换 token;期望 ESO 自动为实例生成 token 并包装为 imagePullSecrets。前置条件
TCR 实例 ID
tcr-xxxxxxxx 已存在(在 TCR 控制台的实例列表页面可查看)。调用者子账号拥有 §3.1.4 中的
tcr:CreateInstanceToken。操作步骤
apiVersion: generators.external-secrets.io/v1alpha1kind: TencentTCRAccessTokenmetadata:name: tencent-tcr-gennamespace: defaultspec:region: ap-guangzhouregistryId: tcr-xxxxxxxx # TCR 实例 IDtokenType: temp # temp(默认, 1h) 或 longtermdesc: "ESO managed token" # tokenType=longterm 时可选auth:secretRef:accessKeyIDSecretRef:name: tencent-credentials # §4 创建的长期凭证 Secretkey: secret-idaccessKeySecretSecretRef:name: tencent-credentialskey: secret-key---apiVersion: external-secrets.io/v1kind: ExternalSecretmetadata:name: tcr-creds-esnamespace: defaultspec:refreshInterval: 30mtarget:name: tcr-credstemplate:type: kubernetes.io/dockerconfigjsondata:.dockerconfigjson: |{"auths":{"tcr-xxxxxxxx.tencentcloudcr.com":{"username":"{{ .username }}","password":"{{ .password }}","auth":"{{ printf "%s:%s" .username .password | b64enc }}"}}}dataFrom:- sourceRef:generatorRef:apiVersion: generators.external-secrets.io/v1alpha1kind: TencentTCRAccessTokenname: tencent-tcr-gen
产出
tcr-creds Secret(type=kubernetes.io/dockerconfigjson),可直接挂到 Pod 的 imagePullSecrets:apiVersion: v1kind: Podmetadata: { name: my-pod, namespace: default }spec:imagePullSecrets:- name: tcr-credscontainers:- name: appimage: tcr-xxxxxxxx.tencentcloudcr.com/my-ns/my-app:v1
说明:
Generator 输出键:
username / password / expires_at(temp 类型还会带 token;longterm 类型额外有 token_id)。kubectl apply -f tcr-gen.yaml
验证
# 1) ExternalSecret Ready=Truekubectl get externalsecret tcr-creds-es -o jsonpath='{.status.conditions}'# 2) 产出的 Secret 类型正确kubectl get secret tcr-creds -o jsonpath='{.type}' ; echo# 期望:kubernetes.io/dockerconfigjson# 3) dockerconfigjson 解码后含 auths 段kubectl get secret tcr-creds -o jsonpath='{.data.\\.dockerconfigjson}' | base64 -d | jq '.auths | keys'# 期望:["tcr-xxxxxxxx.tencentcloudcr.com"]# 4) 用产出的用户名/密码登录测试USER=$(kubectl get secret tcr-creds -o jsonpath='{.data.\\.dockerconfigjson}' | base64 -d | jq -r '.auths["tcr-xxxxxxxx.tencentcloudcr.com"].username')PASS=$(kubectl get secret tcr-creds -o jsonpath='{.data.\\.dockerconfigjson}' | base64 -d | jq -r '.auths["tcr-xxxxxxxx.tencentcloudcr.com"].password')echo "$PASS" | docker login tcr-xxxxxxxx.tencentcloudcr.com -u "$USER" --password-stdin# 期望:Login Succeeded# 5) 真实工作负载拉镜像kubectl run probe --image=tcr-xxxxxxxx.tencentcloudcr.com/my-ns/my-app:v1 \\--overrides='{"spec":{"imagePullSecrets":[{"name":"tcr-creds"}]}}' --restart=Neverkubectl get pod probe -w# 期望:进入 Running,无 ErrImagePull
清理
kubectl delete pod probe --ignore-not-foundkubectl delete externalsecret tcr-creds-eskubectl delete tencenttcraccesstoken tencent-tcr-genkubectl delete secret tcr-creds --ignore-not-found
常见错误
ErrImagePull 且报 unauthorized,说明 dockerconfigjson 里的 host 与镜像地址不同(包含实例 ID + 区域);检查模板里的 tcr-xxxxxxxx.tencentcloudcr.com 是否跟 Pod 镜像完全一致。tokenType=longterm 后 Generator 输出中没 token key:这是正常行为,longterm 类型不返回 token,仅返回 username/password/token_id/expires_at。temp token 1h 后失效,请调低
ExternalSecret.spec.refreshInterval(推荐 30m)。8.7 Generator 字段速查
TencentSTSSessionToken.spec
字段 | 必填 | 说明 |
region | 是 | 腾讯云地域 |
auth.secretRef.accessKeyIDSecretRef | 是 | 长期 SecretId 引用 |
auth.secretRef.accessKeySecretSecretRef | 是 | 长期 SecretKey 引用 |
requestParameters.roleArn | 是 | 被扮演角色 ARN |
requestParameters.roleSessionName | 是 | 长度 2-128,字母数字 =,.@_- |
requestParameters.durationSeconds | 否 | 900-43200,默认 7200 |
requestParameters.policy | 否 | 缩窄权限的 CAM JSON |
requestParameters.externalId | 否 | 跨账号扮演用 |
TencentTCRAccessToken.spec
字段 | 必填 | 说明 |
region | 是 | 腾讯云地域 |
registryId | 是 | TCR 实例 ID( tcr-xxxxxxxx) |
auth.secretRef.* | 是 | 同上 |
tokenType | 否 | temp(默认)或 longterm |
desc | 否 | longterm 类型的描述 |
9. 排错 FAQ
现象 | 排查方向 |
SecretStore Ready=False,message 含 AuthFailure / SecretIdNotFound / SignatureFailure / InvalidAccessKey | |
ExternalSecret Ready=False,message 含 unable to get Tencent STS credentials / AssumeRole | |
PushSecret 创建后远端没出现 | |
删 PushSecret 后远端仍在 | §7.6 的保护:远端 description 不含 ESO managed-by 标识;或 SecretStore recoveryWindowInDays>0,远端是 PendingDelete 状态可恢复 |
Tags 改了但远端没变 | |
OIDC SA 拉不到 Token | SA 上必须有三条 annotation: tke.cloud.tencent.com/providerID、tke.cloud.tencent.com/role-arn、tke.cloud.tencent.com/audience(默认 sts.cloud.tencent.com,与 CAM OIDC 提供商客户端 ID、角色信任策略 oidc:aud 必须三方相等);CAM 信任关系的 oidc:sub 必须与 system:serviceaccount:<ns>:<sa> 完全匹配;SecretStore 的 auth.serviceAccountRef.audiences 字段在腾讯云 Provider 下被忽略,写不写都不生效 |
kubectl apply 报 no matches for kind "PushSecret" 或 TencentSTSSessionToken | CRD 未安装或 chart 版本过旧;用 kubectl get crd | grep external-secrets 确认 |
ExternalSecret 报 nodename nor servname provided 类 DNS 错 | 集群 DNS 解析不到公网 / 内网域名;按需切换到 *.internal.tencentcloudapi.com |
资源策略报无权限( Forbidden cam:CreatePolicy 等) | |
secretPushFormat 与已存在 Secret 不一致 | 远端 Secret 已经是 binary/string 之一时,不能切换;删除重建或在新 SecretKey 推送 |
调试常用命令(把
NAME 替换为实际资源名):kubectl describe secretstore NAMEkubectl describe externalsecret NAMEkubectl describe pushsecret NAME# ESO 控制器默认部署在 kube-systemkubectl -n kube-system logs deploy/external-secrets -f --tail=200