文档中心>容器服务>实践教程>安全>TKE ExternalSecretOperator 实践指南

TKE ExternalSecretOperator 实践指南

最近更新时间:2026-06-03 16:36:10

我的收藏
说明:
本手册面向使用 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 实操 的示例里可以看到:核心的 ExternalSecretexternal-secrets.io/v1,而 PushSecret 实操PushSecretexternal-secrets.io/v1alpha1Generators 实操 的 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.com
kubectl -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-guangzhoudev-*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 凭据的策略

参见 创建自定义策略,选择按策略语法创建,使用空白模板,填写策略名称并粘贴下面的 JSON。
按场景挑选 / 叠加下面的策略片段即可,只读、读写、资源策略、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:DescribeSecretssm:ListSecrets 写链路前置也会用到,但已包含在 基础场景:仅 ExternalSecret 拉取,挂在同一个子账号上即可,不需要在 P1 重复声明。
关于 ssm:PutSecretValueESO 的 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。
仅当使用 PushSecret 的 resourcePolicy 字段(见 PushSecret 实操Generators 实操)时才需要:
{
"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

仅当使用 TencentTCRAccessToken:颁发 TCR 镜像仓库访问令牌TencentTCRAccessToken Generator 拉取 TCR 临时登录凭证时需要。
{
"version": "2.0",
"statement": [
{
"effect": "allow",
"action": [
"tcr:CreateInstanceToken"
],
"resource": [
"qcs::tcr:ap-guangzhou:uin/100000000001:instance/tcr-xxxxxxxx"
]
}
]
}

3.1.5 场景速查

按业务用途选择需要叠加的策略片段:
业务用途
至少需要叠加的策略片段
仅 ExternalSecret 拉取 SSM
§3.1.1
ExternalSecret + PushSecret(不打 Tag)
§3.1.1 + §3.1.2(仅写动作)
PushSecret 且写入时打 Tag
§3.1.1 + §3.1.2(含 Tag 片段)
PushSecret 且使用 resourcePolicy
TCR 镜像仓库访问(TencentTCRAccessToken Generator)
§3.1.4 单独即可;如同账号还需 SSM 拉取/推送,再叠加 §3.1.1 / §3.1.2

3.2 选择鉴权方式并完成授权

把上一步准备好的策略挂到具体的身份上,并让 ESO 知道用哪种方式去用它。三种方式互不混用,配置形态、SecretStore 字段、需要授权的对象都不同:
对比维度
方式一:静态 AKSK
方式二:AKSK + 角色扮演
方式三:TKE OIDC(无 AKSK 落盘)
集群里存什么
子账号的长期 AKSK(K8s Secret)
子账号的长期 AKSK(K8s Secret)
仅一个 ServiceAccount,没有任何 AKSK
真正访问云 API 的身份
子账号本人
被扮演的角色(临时 AKSK)
被扮演的角色(临时 AKSK)
§3.1 的策略挂哪儿
挂在子账号/用户组
挂在角色上(子账号只需 sts:AssumeRole
挂在角色上(OIDC 角色,子账号不参与)
凭据轮转成本
高(手动改 AKSK 并同步进集群)
低(角色策略可在线改,临时凭证 自动过期)
最低(无长期凭据)
跨账号
不支持
支持(角色实体填对方主账号)
不支持
SecretStore 形态
auth.secretRef§5.1
auth.secretRef + 顶级 role 字段(§5.1 变体)
auth.serviceAccountRef§5.2

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
获取步骤 1 中创建的子账号的 SecretId / SecretKey,并写入 K8s Secret,详见 §4
对应的 SecretStore:见 §5.1(不带 role 字段的形态)。

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
获取步骤 1 中创建的子账号的 SecretId / SecretKey,并写入 K8s Secret,详见 §4
对应的 SecretStore:见 §5.1(带顶级 role 字段的形态)。

3.2.3 方式三:通过 TKE OIDC 授权(无 AKSK 落盘)

通过 TKE 集群的 OIDC 身份提供商授权时,不需要在集群里存放任何长期 AKSK:ESO 控制器以 ServiceAccount 身份运行,K8s 自动为 Pod 投影出短时 OIDC Token,腾讯云 STS 凭此颁发临时凭证。该方式适用于对凭据落盘有合规要求希望按工作负载粒度做最小授权的场景。
授权步骤
步骤 1:开启集群的 ServiceAccountIssuerDiscovery(OIDC 资源访问控制)
进入 TKE 控制台,选择目标集群,在基本信息页面找到 ServiceAccountIssuerDiscovery 配置项,点击右侧编辑
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-issuerservice-account-jwks-uri 参数值由 TKE 按默认规则下发,不允许手工编辑。该步骤会在集群中启用 SA Token 投影,并在腾讯云侧暴露集群的 OIDC 配置端点,供 CAM 进行联合身份校验。
步骤 2:在 CAM 中创建 OIDC 身份提供商(如自动创建可跳过)
如果步骤 1 没有勾选创建 CAM OIDC 提供商,需要手动创建:
进入 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: v1
kind: ServiceAccount
metadata:
name: my-sa
namespace: my-ns
annotations:
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。
对应的 SecretStore:见 §5.2auth.serviceAccountRef 形态)。

4. 准备凭证 Secret

# tencent-credentials.yaml
apiVersion: v1
kind: Secret
metadata:
name: tencent-credentials
namespace: default
type: Opaque
stringData:
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/v1
kind: SecretStore
metadata:
name: tencent-ssm-store
namespace: default
spec:
provider:
tencent:
regionID: ap-guangzhou
auth:
secretRef:
accessKeyIDSecretRef:
name: tencent-credentials
key: secret-id
accessKeySecretSecretRef:
name: tencent-credentials
key: secret-key
形态 B:方式二 AKSK + 角色扮演(多一个顶级 role 字段)
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: tencent-ssm-store-assumed
namespace: default
spec:
provider:
tencent:
regionID: ap-guangzhou
role: qcs::cam::uin/100000000001:roleName/eso-runtime-role # 见 §3.2.2
auth:
secretRef:
accessKeyIDSecretRef:
name: tencent-credentials
key: secret-id
accessKeySecretSecretRef:
name: tencent-credentials
key: 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/v1
kind: SecretStore
metadata:
name: tencent-ssm-store-oidc
namespace: my-ns
spec:
provider:
tencent:
regionID: ap-guangzhou
auth:
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/v1
kind: ExternalSecret
metadata:
name: oidc-pull-app
namespace: my-ns
spec:
refreshInterval: 1m
secretStoreRef:
kind: SecretStore
name: tencent-ssm-store-oidc # §5.2 创建的 OIDC SecretStore
target:
name: oidc-pull-app
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: dev-app-oidc-sample # 远端凭据名(由运维侧用 tccli 创建)
version: v1 # 必填原因详见 §6 通用提示
property: username # 可选:JSON property
- secretKey: password
remoteRef:
key: dev-app-oidc-sample
version: v1
property: password

5.3 ClusterSecretStore:跨命名空间共享

如果你希望多个命名空间共用同一份 SSM 配置,使用 ClusterSecretStore。它是集群级资源,凭证 Secret 引用必须显式带上 namespace
说明:
本示例对应 3.2.1 方式一:通过 AKSK 授权auth.secretRef 直接读取 K8s Secret 中的 SecretId/SecretKey)。
如果你使用方式二/方式三,请把 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/v1
kind: ClusterSecretStore
metadata:
name: tencent-ssm-cluster-store
spec:
provider:
tencent:
regionID: ap-guangzhou
auth:
secretRef:
accessKeyIDSecretRef:
name: tencent-credentials
namespace: default # ClusterSecretStore 必须显式 namespace
key: secret-id
accessKeySecretSecretRef:
name: tencent-credentials
namespace: default
key: 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-store
kind: ClusterSecretStore

5.4 SSM 行为参数:删除窗口与重试

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: tencent-ssm-store
spec:
provider:
tencent:
regionID: ap-guangzhou
prefix: "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: 3
rateLimitExceededMaxRetries: 5
参数
取值
说明
prefix
string
所有 Secret 名自动加前缀,读写都生效;用于多环境/多团队隔离
ssm.recoveryWindowInDays
01-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/v1
kind: ExternalSecret
metadata:
name: app-db-es
namespace: default
spec:
refreshInterval: 1h
secretStoreRef:
name: tencent-ssm-store
kind: SecretStore
target:
name: app-db # 产出的 K8s Secret 名
creationPolicy: Owner
data:
- secretKey: DB_USER # K8s Secret 中的键
remoteRef:
key: dev-app-db # SSM 中的 SecretName
property: username # JSON 路径;非 JSON 凭据省略
- secretKey: DB_PASS
remoteRef:
key: dev-app-db
property: password
kubectl apply -f app-db-es.yaml

验证

# 1) ExternalSecret 状态:必须 Ready=True,SecretSynced=True
kubectl 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 ; echo
kubectl get secret app-db -o jsonpath='{.data.DB_PASS}' | base64 -d ; echo
控制台校验:在 SSM 控制台的凭据管理系统页面搜索 dev-app-db,确认查看凭据值与上面解码内容一致。

清理

kubectl delete externalsecret app-db-es
kubectl 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/v1
kind: ExternalSecret
metadata:
name: app-db-all-es
namespace: default
spec:
refreshInterval: 1h
secretStoreRef: { 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-dbdev-app-redisdev-app-mq,希望它们打包成同一个 K8s Secret 并随远端一起增删。

前置条件

同 6.1,且子账号额外需要 ssm:ListSecrets(已在 §3.1.1 一并给出)。

操作步骤

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: app-bundle-es
namespace: default
spec:
refreshInterval: 5m
secretStoreRef: { 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:给已有凭据补 tag
tccli tag AttachResourcesTag --ServiceType ssm --ResourcePrefix secret \\
--ResourceRegion ap-guangzhou --ResourceIds '["dev-app-db"]' \\
--TagKey env --TagValue prod

操作步骤

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: app-tagged-es
namespace: default
spec:
refreshInterval: 5m
secretStoreRef:
name: tencent-ssm-store
kind: SecretStore
target:
name: app-tagged
creationPolicy: Owner
dataFrom:
- find:
tags:
env: prod
team: backend
kubectl apply -f app-tagged-es.yaml

验证

kubectl describe externalsecret app-tagged-es | sed -n '/Status/,$p'
# 看 status.binding.name 与 events,没有 reason=ValidationFailed/SecretSyncedError
kubectl 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 --TagFiltersInvalidParameterValue.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/v1
kind: ExternalSecret
metadata:
name: app-db-v2-es
namespace: default
spec:
refreshInterval: 1h
secretStoreRef:
name: tencent-ssm-store
kind: SecretStore
target:
name: app-db-v2
creationPolicy: Owner
data:
- secretKey: db-password
remoteRef:
key: dev-app-db
property: password
version: "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/v1
kind: ExternalSecret
metadata:
name: app-tls-es
namespace: default
spec:
refreshInterval: 1h
secretStoreRef:
name: tencent-ssm-store
kind: SecretStore
target:
name: app-tls
creationPolicy: Owner
data:
- secretKey: tls.crt
remoteRef:
key: dev-app-tls
decodingStrategy: 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/v1
kind: ExternalSecret
metadata: { name: app-cfg-es, namespace: default }
spec:
refreshInterval: 1h
secretStoreRef: { name: tencent-ssm-store, kind: SecretStore }
target:
name: app-cfg
template:
engineVersion: v2
type: Opaque
data:
application.properties: |
db.username={{ .username }}
db.password={{ .password }}
jdbc.url=jdbc:mysql://{{ .host }}:{{ .port }}/app
dataFrom:
- 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 文本,{{ }} 已被实际值替换
场景二:渲染为 kubernetes.io/dockerconfigjson 直接给 imagePullSecrets 使用,参见 8.6 TencentTCRAccessToken

清理

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=truekubectl label ns team-a eso-target=true)。

操作步骤

场景:同一份 SSM 凭据要复制到多个 namespace。
apiVersion: external-secrets.io/v1
kind: ClusterExternalSecret
metadata:
name: app-db-ces
spec:
externalSecretName: app-db # 在每个目标 ns 创建的 ES 名
namespaceSelectors:
- matchLabels:
eso-target: "true"
externalSecretSpec:
refreshInterval: 1h
secretStoreRef: { 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 --overwrite
kubectl 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 和目标 Secret
for ns in $(kubectl get ns -l eso-target=true -o name | awk -F/ '{print $2}'); do
echo "== $ns =="
kubectl -n "$ns" get externalsecret app-db -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' ; echo
kubectl -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-dbpassword,希望 K8s Secret 在 1 分钟内自动跟进,避免应用读到旧密码。

前置条件

同 6.1。refreshInterval 越短,对 SSM API QPS 配额压力越大,建议生产环境 ≥ 30s。

操作步骤

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: app-db-fast-es
namespace: default
spec:
refreshInterval: 1m # 默认 1h;最低 1s 但不建议低于 30s
secretStoreRef:
name: tencent-ssm-store
kind: SecretStore
target:
name: app-db
creationPolicy: Owner
data:
- secretKey: DB_PASS
remoteRef:
key: dev-app-db
property: 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}' ; echo
kubectl 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/v1
kind: ExternalSecret
metadata:
name: app-db-policy-es
namespace: default
spec:
refreshInterval: 1h
secretStoreRef:
name: tencent-ssm-store
kind: SecretStore
target:
name: app-db
creationPolicy: Owner # Owner(默认)|Orphan|Merge|None
deletionPolicy: Retain # Retain(默认)|Delete|Merge
data:
- secretKey: DB_PASS
remoteRef:
key: dev-app-db
property: 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-es
kubectl 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 Secret
apiVersion: v1
kind: Secret
metadata:
name: app-secret
namespace: default
type: Opaque
stringData:
db-password: "super-secret-password"
---
# 2) PushSecret
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: push-app-secret
namespace: default
spec:
refreshInterval: 1h
secretStoreRefs:
- name: tencent-ssm-store
kind: SecretStore
selector:
secret:
name: app-secret
data:
- match:
secretKey: db-password # 来自 K8s Secret 的 key
remoteRef:
remoteKey: prod-app-db-password # SSM 中的 Secret 名
kubectl apply -f app-secret-and-push.yaml

验证

# 1) PushSecret Ready=True
kubectl 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/v1alpha1
kind: PushSecret
metadata:
name: push-with-meta
spec:
secretStoreRefs:
- { name: tencent-ssm-store, kind: SecretStore }
selector:
secret: { name: app-secret }
data:
- match:
secretKey: db-password
remoteRef:
remoteKey: prod-app-db-password
metadata:
apiVersion: kubernetes.external-secrets.io/v1alpha1
kind: PushSecretMetadata
spec:
description: "Database credentials for prod"
tags:
env: production
team: backend
kmsKeyId: kms-xxxxxxxx # 可选;不填则使用 SSM 平台默认 CMK
secretPushFormat: 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/v1alpha1
kind: PushSecret
metadata:
name: push-property
spec:
refreshInterval: 30s
secretStoreRefs:
- { name: tencent-ssm-store, kind: SecretStore }
selector:
secret: { name: app-secret } # 假设其中有 key: db-password
data:
- match:
secretKey: db-password
remoteRef:
remoteKey: prod-app-config
property: 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 编码)写回;兼容两种格式解出 JSON
RESP=$(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); fi
echo "$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) 滚动源 Secret
kubectl 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/v1alpha1
kind: PushSecret
metadata:
name: push-app-secret
namespace: default
spec:
refreshInterval: 1h
deletionPolicy: Delete # 删 PushSecret 时同步删远端
secretStoreRefs:
- name: tencent-ssm-store
kind: SecretStore
selector:
secret:
name: app-secret
data:
- match:
secretKey: db-password
remoteRef:
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-by
tccli ssm CreateSecret --SecretName prod-app-manual --VersionId v0 \\
--SecretString 'manual-value' --Description 'manually created by ops'

# 2) 创建 PushSecret 接管该远端,并显式 deletionPolicy: Delete
cat <<'EOF' | kubectl apply -f -
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: push-manual
namespace: default
spec:
refreshInterval: 1h
deletionPolicy: Delete
secretStoreRefs:
- name: tencent-ssm-store
kind: SecretStore
selector:
secret:
name: app-secret
data:
- match:
secretKey: db-password
remoteRef:
remoteKey: prod-app-manual
EOF
说明:
因为 ESO 写入固定 VersionId=v_eso_latest,而手动创建的远端版本是 v0,PushSecret 的同步阶段可能报 ResourceNotFound: can not find secrets(这是 ESO 自身保护,它不会把数据强行写入它不认识的版本流上)。这与“删除时不动远端”是两条独立的保护。

验证(保护机制)

# 前提:prod-app-manual 是控制台手动创建的,描述不含 managed-by
kubectl delete pushsecret push-manual
sleep 15
tccli ssm DescribeSecret --SecretName prod-app-manual | jq '{Description, Status}'
# 期望:Description 仍是手动设置的内容(未被 ESO 覆盖),Status=Enabled(远端未被删除)

清理

# 远端确认无用后再手动清理
tccli ssm DisableSecret --SecretName prod-app-manual
tccli 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/v1alpha1
kind: PushSecret
metadata:
name: push-with-policy
namespace: default
spec:
refreshInterval: 1h
secretStoreRefs:
- name: tencent-ssm-store
kind: SecretStore
selector:
secret:
name: app-secret
data:
- match:
secretKey: db-password
remoteRef:
remoteKey: prod-app-db-password
metadata:
apiVersion: kubernetes.external-secrets.io/v1alpha1
kind: PushSecretMetadata
spec:
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.resourcePolicyapply,ESO 会自动调 cam:DeletePolicy 清理;但直接 kubectl delete pushsecret 不会触发该清理(即使 deletionPolicy: Delete 也只会删 SSM 凭据本身,不动 CAM 策略)。推荐的清理顺序见下面“清理”节。
需要 §3.1.3 的额外权限。
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: v1
kind: ConfigMap
metadata:
name: app-policy
namespace: default
data:
policy.json: |
{"version":"2.0","statement":[{"effect":"allow","principal":{"qcs":["qcs::cam::uin/100000000001:uin/200000000002"]},"action":["ssm:GetSecretValue"],"resource":["*"]}]}
---
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: push-with-policy-ref
spec:
secretStoreRefs:
- { name: tencent-ssm-store, kind: SecretStore }
selector:
secret: { name: app-secret }
data:
- match:
secretKey: db-password
remoteRef:
remoteKey: prod-app-db-password
metadata:
apiVersion: kubernetes.external-secrets.io/v1alpha1
kind: PushSecretMetadata
spec:
resourcePolicy:
policySourceRef:
kind: ConfigMap # 或 Secret
name: app-policy
key: policy.json
# namespace 可省略,省略时默认 SecretStore 所在命名空间
说明:
policyDocumentpolicySourceRef 互斥,二选一。
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-ref
kubectl 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/v1alpha1
kind: PushSecretMetadata
spec:
tags: { env: production, team: backend } # 仅创建时生效
description: "..." # 默认 secret 'managed-by:external-secrets'
kmsKeyId: kms-xxxxxxxx # 可选
secretPushFormat: binary # binary(默认) 或 string
resourcePolicy:
policyDocument: |
{"version":"2.0","statement":[...]}
# 或者:
policySourceRef:
kind: ConfigMap # ConfigMap 或 Secret
name: app-policy
key: policy.json
namespace: "" # 可省略

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::stsFactorygetSTSEndpoint()TENCENT_STS_ENDPOINT
generators/v1/tencenttcr/tencenttcr.go::defaultTCRFactorygetTCREndpoint()TENCENT_TCR_ENDPOINT
providers/v1/tencent/util/provider.goDefaultTCREndpoint / GetTCREndpoint()
apis/externalsecrets/v1v1beta1generatorRef.kind enum 补 TencentTCRAccessToken
Helm chart rbac.yaml 的 ClusterRole 补 tencenttcraccesstokens list/watch/get
VPC 部署:见 §2 末尾“内网 endpoint 注入”,5 个 TENCENT_*_ENDPOINT 必须齐全(其中 TENCENT_TCR_ENDPOINT 是历史最容易漏的一项)。
实跑结果:见 §8 表,§8.1§8.6 现已全部 (§8.5 主场景通,附带发现 ClusterGenerator namespace 透传 know-bug 单独跟踪)。

8.1 案例 S TencentSTSSessionToken:动态颁发腾讯云临时凭证

业务场景

集群内工作负载需要访问腾讯云 SSM,但不希望长期 AKSK 落在集群里;期望 ESO 定期去 STS 换临时凭证并物化为 K8s Secret。

前置条件

已按 §3.2.2 创建被扮演角色(eso-runtime-role),信任关系允许调用者子账号 AssumeRole。
调用者(本例的长期 Secret 对应者)拥有 sts:AssumeRole 权限(§3.2.2 步骤 3)。

操作步骤

依赖:§3.2.2 中创建好的角色(roleArn)。
# 长期身份 Secret —— 用来调 STS 换临时凭证
apiVersion: v1
kind: Secret
metadata:
name: tencent-long-term
namespace: default
type: Opaque
stringData:
secret-id: "AKIDxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
secret-key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
---
# Generator
apiVersion: generators.external-secrets.io/v1alpha1
kind: TencentSTSSessionToken
metadata:
name: tencent-sts-gen
namespace: default
spec:
region: ap-guangzhou
auth:
secretRef:
accessKeyIDSecretRef:
name: tencent-long-term
key: secret-id
accessKeySecretSecretRef:
name: tencent-long-term
key: secret-key
requestParameters:
roleArn: "qcs::cam::uin/100000000001:roleName/eso-runtime-role"
roleSessionName: "eso-runtime-session"
---
# 让 ESO 把 Generator 输出物化到一个 K8s Secret
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: tencent-sts-creds-es
namespace: default
spec:
refreshInterval: 30m # 每 30 分钟换一次临时凭证
target:
name: tencent-sts-creds # 产出 Secret 名
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: TencentSTSSessionToken
name: 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=True
kubectl 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-es
kubectl delete tencentstssessiontoken tencent-sts-gen
kubectl 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/v1
kind: SecretStore
metadata:
name: tencent-ssm-sts-store
namespace: default
spec:
provider:
tencent:
regionID: ap-guangzhou
auth:
secretRef:
accessKeyIDSecretRef:
name: tencent-sts-creds
key: access_key_id
accessKeySecretSecretRef:
name: tencent-sts-creds
key: secret_access_key
sessionTokenSecretRef: # ← 关键:使用临时 Token
name: tencent-sts-creds
key: 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) 用它跑一个 ExternalSecret
cat <<EOF | kubectl apply -f -
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata: { name: sts-probe-es, namespace: default }
spec:
refreshInterval: 1m
secretStoreRef: { name: tencent-ssm-sts-store, kind: SecretStore }
target: { name: sts-probe }
data:
- secretKey: probe
remoteRef: { key: dev-app-db, property: username }
EOF

kubectl 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-es
kubectl 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 与 expiration
for i in 1 2 3; do
RV=$(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-guangzhou
auth: { ... }
requestParameters:
roleArn: "qcs::cam::uin/100000000001:roleName/eso-runtime-role"
roleSessionName: "eso-runtime-session"
durationSeconds: 3600 # 范围 900-43200,默认 7200
policy: | # 进一步缩小临时凭证权限范围
{"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-ateam-b ……)都需要事先存在一份同名的长期凭证 Secret(如 tencent-long-term)。

操作步骤

集群级资源,配合 ExternalSecret.dataFrom.sourceRef.generatorRef.kind: ClusterGenerator 使用。注意 yaml 里 secretRef 不写 namespace——它会被忽略,控制器始终在 ExternalSecret 所在 ns 查找 tencent-long-term
apiVersion: generators.external-secrets.io/v1alpha1
kind: ClusterGenerator
metadata:
name: tencent-sts-cluster-gen
spec:
kind: TencentSTSSessionToken
generator:
tencentSTSSessionTokenSpec:
region: ap-guangzhou
auth:
secretRef:
accessKeyIDSecretRef:
name: tencent-long-term # ← 必须存在于消费它的 ExternalSecret 所在 ns
key: secret-id
accessKeySecretSecretRef:
name: tencent-long-term
key: secret-key
requestParameters:
roleArn: "qcs::cam::uin/100000000001:roleName/eso-runtime-role"
roleSessionName: "eso-cluster-session"
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: any-ns-sts
namespace: team-a # 该 ns 下需预先存在 tencent-long-term
spec:
refreshInterval: 30m
target: { name: tencent-sts-creds }
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: ClusterGenerator
name: tencent-sts-cluster-gen
# 1) 每个目标 ns 落一份同名长期 Secret(示例直接从 default 复制)
for NS in team-a team-b; do
kubectl 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 + ExternalSecret
kubectl 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-sts
kubectl 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/v1alpha1
kind: TencentTCRAccessToken
metadata:
name: tencent-tcr-gen
namespace: default
spec:
region: ap-guangzhou
registryId: tcr-xxxxxxxx # TCR 实例 ID
tokenType: temp # temp(默认, 1h) 或 longterm
desc: "ESO managed token" # tokenType=longterm 时可选
auth:
secretRef:
accessKeyIDSecretRef:
name: tencent-credentials # §4 创建的长期凭证 Secret
key: secret-id
accessKeySecretSecretRef:
name: tencent-credentials
key: secret-key
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: tcr-creds-es
namespace: default
spec:
refreshInterval: 30m
target:
name: tcr-creds
template:
type: kubernetes.io/dockerconfigjson
data:
.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/v1alpha1
kind: TencentTCRAccessToken
name: tencent-tcr-gen
产出 tcr-creds Secret(type=kubernetes.io/dockerconfigjson),可直接挂到 Pod 的 imagePullSecrets
apiVersion: v1
kind: Pod
metadata: { name: my-pod, namespace: default }
spec:
imagePullSecrets:
- name: tcr-creds
containers:
- name: app
image: 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=True
kubectl 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=Never
kubectl get pod probe -w
# 期望:进入 Running,无 ErrImagePull

清理

kubectl delete pod probe --ignore-not-found
kubectl delete externalsecret tcr-creds-es
kubectl delete tencenttcraccesstoken tencent-tcr-gen
kubectl 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
AKSK 错误或被禁用;检查 4. 准备凭证 Secret
ExternalSecret Ready=False,message 含 unable to get Tencent STS credentials / AssumeRole
长期身份缺少 sts:AssumeRole 权限,或被扮演角色信任关系不允许它扮演(见 §3.2
PushSecret 创建后远端没出现
子账号/角色缺少 ssm:CreateSecret(见 §3.1.2);或被 prefix 改写了 Secret 名,控制台需带前缀搜索
删 PushSecret 后远端仍在
§7.6 的保护:远端 description 不含 ESO managed-by 标识;或 SecretStore recoveryWindowInDays>0,远端是 PendingDelete 状态可恢复
Tags 改了但远端没变
SSM 限制:Tags 仅在创建时生效,无法增量更新(见 §7.2
OIDC SA 拉不到 Token
SA 上必须有三条 annotation:tke.cloud.tencent.com/providerIDtke.cloud.tencent.com/role-arntke.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 applyno matches for kind "PushSecret"TencentSTSSessionToken
CRD 未安装或 chart 版本过旧;用 kubectl get crd | grep external-secrets 确认
ExternalSecretnodename nor servname provided 类 DNS 错
集群 DNS 解析不到公网 / 内网域名;按需切换到 *.internal.tencentcloudapi.com
资源策略报无权限(Forbidden cam:CreatePolicy 等)
缺少 §3.1.3 的 CAM 策略管理权限
secretPushFormat 与已存在 Secret 不一致
远端 Secret 已经是 binary/string 之一时,不能切换;删除重建或在新 SecretKey 推送
调试常用命令(把 NAME 替换为实际资源名):
kubectl describe secretstore NAME
kubectl describe externalsecret NAME
kubectl describe pushsecret NAME
# ESO 控制器默认部署在 kube-system
kubectl -n kube-system logs deploy/external-secrets -f --tail=200