Kubernetes 基础体系 · 第 48/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Kubernetes Secret 治理:etcd 加密、KMS、External Secrets、轮换和泄漏
Kubernetes Secret 用于保存密码、令牌、私钥、证书等小型敏感数据。它解决的是“如何把敏感数据作为 Kubernetes API 对象分发给 Pod”,并不等于“数据已经安全保存”或“应用已经具备安全的密钥管理能力”。
完整的治理链路至少包含以下边界:
外部密钥系统
│
│ External Secrets 控制器读取
▼
Kubernetes API Server ──加密存储──> etcd
│
│ 鉴权、授权、审计、准入
▼
Kubelet ──临时卷 / 环境变量──> Pod
│
▼
应用进程、日志、调试工具、下游服务
每一段都有不同的风险:
etcd加密主要保护“静态存储中的 Secret”;- KMS 解决的是“用于加密 Secret 的密钥如何由独立密钥系统保护”;
- External Secrets 解决的是“外部密钥系统中的值如何同步为 Kubernetes Secret”;
- 轮换解决的是“旧凭据如何失效、新凭据如何传播以及应用如何重新加载”;
- 泄漏治理则要覆盖 API、etcd、节点、容器、日志、CI/CD 和外部密钥系统,而不是只检查
Secretmanifest。
1. Secret 的真实数据模型:编码不是加密
一个典型的 Secret 如下:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: payments
type: Opaque
stringData:
username: app
password: "correct-horse-battery-staple"
stringData 只是客户端提交 Secret 的便利字段。API Server 接收后会把它转换为 data,而 data 的值使用 Base64 表示:
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
namespace: payments
type: Opaque
data:
username: YXBw
password: Y29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ==
Base64 只是一种编码:
printf '%s' 'Y29ycmVjdC1ob3JzZS1iYXR0ZXJ5LXN0YXBsZQ==' | base64 -d
# correct-horse-battery-staple
因此,下列命令并没有解密 Secret:
kubectl get secret db-credentials -n payments \
-o jsonpath='{.data.password}' | base64 -d
它只是:
- 通过 Kubernetes API 读取 Secret;
- 经过 RBAC 授权;
- 取出
data.password; - 做 Base64 解码。
如果一个主体具有 get 权限,那么是否使用 data 还是 stringData 不影响其读取能力。
1.1 Secret 存在哪里
Secret 是 Kubernetes API 对象。典型写入路径是:
kubectl / controller / kubelet
│ HTTPS
▼
API Server
│ 序列化并写入
▼
etcd
没有配置静态加密时,Secret 的序列化内容通常可以在 etcd 中被直接恢复。即使 Kubernetes API 对外强制使用 TLS,也不能推出“etcd 中的 Secret 已经加密”:
- TLS 保护传输链路;
- etcd 加密保护存储内容;
- RBAC 保护 API 访问;
- 节点和容器运行时还存在另一组访问路径。
这些控制彼此不能替代。
1.2 Secret 不是任意大文件存储
Kubernetes Secret 面向密码、令牌、证书等小型数据。API Server 默认受对象大小限制影响,实际部署还会受到 etcd、网络、序列化和控制器处理成本的约束。大文件、证书包或二进制密钥材料不应因为可以 Base64 就全部塞进 Secret。
对于大型或高价值密钥,通常应只在 Kubernetes 中保存短期访问凭据、引用或解密所需的最小材料,让应用直接访问专用密钥系统。
2. Secret 的安全边界:谁能看到它
Secret 的安全性首先由 Kubernetes 的认证和授权模型决定。
2.1 get、list、watch 不是同一种风险
一个主体能够读取单个对象,需要类似以下权限:
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["db-credentials"]
verbs: ["get"]
但实际授权还需要考虑:
list secrets可以批量获得命名空间中的所有 Secret;watch secrets可以持续观察对象变化;list和watch往往是控制器正常工作的必要权限;- 跨命名空间的
ClusterRole和ClusterRoleBinding会扩大影响范围; - 能创建或修改 Pod 的主体,可能通过 Pod 的
secret卷、环境变量或envFrom间接取得 Secret。
最后一点很重要:即使某个用户不能 get secret,如果他能在目标命名空间创建一个 Pod,并且知道目标 Secret 名称,他可能可以让 Kubelet 把 Secret 注入自己的 Pod。因而 Secret 读取权限不能只通过 secrets 资源规则判断,还必须检查 Pod 创建、Deployment 修改、Job 创建等间接能力。
2.2 etcd 管理权限是更高层的边界
能够读取 etcd 数据库文件、执行 etcd 查询或取得 etcd 快照的人,通常绕过了 Kubernetes API 的普通 RBAC。此时是否安全取决于:
- Secret 是否启用了 API Server 的静态加密;
- 加密密钥是否与 etcd 数据分离;
- etcd 快照和备份是否也被保护;
- 恢复环境是否同样配置了加密密钥;
- 运维人员和备份系统是否具有访问密钥的权限。
因此,kubectl auth can-i get secrets 只能回答 Kubernetes API 授权问题,不能回答 etcd、节点磁盘、备份平台和 KMS 的授权问题。
3. etcd 静态加密:API Server 如何保护 Secret
Kubernetes 的静态加密由 API Server 的 --encryption-provider-config 配置启用。它作用于 API Server 写入存储层的数据,常见目标是 secrets。
一个使用本地 AES-CBC 密钥的示例:
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- aescbc:
keys:
- name: key1
secret: <base64-encoded-32-byte-key>
- identity: {}
其中:
resources指定要加密的资源;aescbc是一种本地加密提供者;secret是 Base64 编码后的密钥材料,不是任意字符串;identity表示不加密、原样存储;providers的顺序同时影响写入和读取。
3.1 写入和读取规则
对于一个资源,API Server 通常按下面的规则工作:
- 写入新对象时,选择列表中第一个可用的 provider;
- 读取对象时,按 provider 顺序尝试解密;
- 旧 provider 仍然保留在配置中,才能读取尚未迁移的旧数据;
- 只有重新写入对象,旧密文才会转换成新 provider 生成的密文。
这意味着下面的配置存在明显风险:
providers:
- identity: {}
- aescbc:
keys:
- name: key1
secret: ...
因为 identity 排在第一位,新写入的数据会优先以明文存储。即使后面配置了 aescbc,也不能改变这个写入结果。
更安全的迁移形态是:
providers:
- aescbc:
keys:
- name: key2
secret: <new-key>
- aescbc:
keys:
- name: key1
secret: <old-key>
- identity: {}
这里的 identity 只作为读取旧明文对象的回退项。待所有对象完成重写后,应移除 identity,否则未来误写或新资源配置都可能重新产生明文对象。
3.2 读取加密配置不能只看配置文件
配置文件存在不代表功能已经生效。实际部署中需要检查:
- 所有 API Server 实例是否使用同一份有效配置;
- 配置文件是否挂载到正确路径;
- API Server 是否已经重启或动态加载成功;
- 配置中的密钥是否完整、格式正确;
- 高可用集群中是否有旧实例仍使用旧配置;
- 旧对象是否真的完成了重写。
如果多个 API Server 的配置不一致,可能出现以下状态:
- 一个实例能读取对象,另一个实例无法读取;
- 新写入对象由不同实例使用不同密钥;
- 轮换后部分对象仍只能由旧实例解密;
- API Server 启动失败或反复重启。
3.3 如何验证 etcd 中是否已经加密
不要直接把生产 etcd 快照导出到普通工作站。验证时应使用受控环境,并注意命令输出本身可能包含敏感数据。
一个常见的验证思路是:
- 创建一个只用于测试的 Secret;
- 读取 API Server 返回的 Secret,确认业务端仍可正常使用;
- 在受控的 etcd 检查环境中搜索对应对象;
- 检查 etcd 中是否出现明文值;
- 检查加密后的对象是否带有加密 provider 的存储标记。
不同 Kubernetes 版本和 provider 的内部存储前缀可能不同,不应把某个前缀硬编码为跨版本合规判断。更可靠的验证是使用官方支持的加密配置和迁移流程,并在备份、恢复环境中做实际恢复测试。
4. 加密 provider 与密钥轮换
4.1 aescbc、aesgcm、secretbox 和 kms
Kubernetes 提供多个静态加密 provider。它们的差异主要在于:
- 加密操作在 API Server 本地完成还是交给 KMS;
- 密钥材料是否存放在 API Server 主机;
- provider 的安全性质、运维方式和版本支持情况;
- 是否需要外部进程和 Unix Domain Socket。
对生产治理而言,关键问题不是只选择某个算法名称,而是回答:
加密密钥本身由谁持有?API Server 主机失陷后,攻击者能否同时取得密文和解密密钥?
如果加密密钥直接写在 API Server 使用的配置文件中,那么攻击者取得该文件后可能解密 etcd 中的 Secret。该方案仍然有价值,因为它能防止“只有 etcd 数据或快照泄漏”的场景,但不能把它描述为密钥隔离。
4.2 KMS 的定义
KMS 是 Key Management Service,密钥管理服务。Kubernetes 的 KMS provider 让 API Server 通过 KMS 插件访问外部密钥系统,而不是把主加密密钥直接作为普通配置存储在 API Server 上。
典型数据流如下:
API Server
│
│ KMS gRPC / Unix socket
▼
KMS 插件
│
│ 云 KMS、HSM 或其他外部密钥服务
▼
外部 KEK
这里通常涉及两层密钥:
KEK:Key Encryption Key,用于保护数据加密密钥;DEK:Data Encryption Key,用于加密具体的 Secret 数据。
抽象过程可以写成:
密文 = Encrypt(DEK, Secret)
封装后的 DEK = Encrypt(KMS-KEK, DEK)
存储 = 密文 + 封装后的 DEK + 版本信息
读取时,API Server 或 KMS 插件先恢复 DEK,再解密 Secret。具体封装格式和缓存行为由 Kubernetes 版本及 KMS 插件实现决定,但治理上的关键点不变:etcd 不应单独拥有恢复 Secret 所需的完整密钥材料。
4.3 KMS v1 和 KMS v2
Kubernetes KMS provider 使用插件协议。KMS v1 和 KMS v2 在缓存、协议行为和插件交互上存在差异。
- KMS v1 已经进入弃用路径,具体可用性取决于 Kubernetes 版本;
- KMS v2 是当前应优先评估的方向;
- 云厂商托管控制平面可能不允许用户自行配置 API Server 的 provider;
- 某些发行版会提供自己的 KMS 集成,配置参数和升级策略不完全相同。
因此,不能把某个云厂商的 kms 配置片段直接当作所有 Kubernetes 集群通用配置。应以目标 Kubernetes 版本的官方文档、发行版文档和 KMS 插件版本说明为准。
无论使用哪一种协议,KMS 插件故障都会影响 API Server:
- 新建或更新 Secret 可能失败;
- API Server 读取已加密对象可能失败;
- 控制器可能持续重试;
- Pod 使用 Secret 的新调度或重建可能无法完成;
- 如果 KMS 配置阻止 API Server 启动,影响范围会扩大到整个 API 服务。
KMS 不是一个“零成本开关”。必须验证:
- KMS 插件进程和 Unix socket 权限;
- KMS 对象和密钥版本;
- API Server 到插件的连通性;
- 插件到外部 KMS 的网络和身份;
- KMS 限流、配额和不可用时的行为;
- 集群恢复时能否访问原有密钥;
- 密钥禁用、轮换和删除的审批流程。
4.4 静态加密密钥轮换的完整步骤
假设当前使用 key1,要切换到 key2,安全过程不是简单替换配置:
第一步:把新 provider 放在第一位
providers:
- aescbc:
keys:
- name: key2
secret: <new-key>
- aescbc:
keys:
- name: key1
secret: <old-key>
- identity: {}
新写入对象使用 key2,旧对象仍可使用 key1 或 identity 读取。
第二步:逐步重写目标对象
最简单的概念性操作是重新提交对象,使 API Server 产生一次写入:
kubectl get secret db-credentials -n payments -o yaml \
| kubectl replace -f -
这条命令的风险包括:
- YAML 可能包含敏感数据;
- 管道进程、终端历史、调试日志可能暴露内容;
replace可能因为resourceVersion过期而失败;- 过大的批量操作可能给 API Server、etcd 和控制器造成压力;
- 对象中不适合由客户端回写的字段可能引发冲突;
- 误处理
kubernetes.io/service-account-token等系统对象会扩大影响。
生产环境应采用分批、限速、可重试的迁移程序,并记录对象名称、版本和失败原因,而不是把所有 Secret 一次性导出到文件。迁移程序还必须避免把 data 写入普通日志。
第三步:确认旧格式已清空
需要确认所有目标对象都已经由新 provider 写入。不能只检查最近创建的 Secret,因为旧对象可能一直未被修改。
第四步:移除旧 provider
迁移完成后,才可以删除旧密钥和 identity 回退:
providers:
- aescbc:
keys:
- name: key2
secret: <new-key>
先删除旧 provider 再重写对象,会导致仍使用旧密钥或仍为明文的对象无法读取。
5. External Secrets:把外部密钥系统同步为 Kubernetes Secret
External Secrets 通常指 External Secrets Operator(ESO)及类似控制器提供的 CRD 和控制循环。它不是 Kubernetes 核心 API 的内建资源,而是一个第三方扩展。不同项目、版本和云厂商 provider 的字段可能不同。
它解决的不是 etcd 加密,而是数据来源问题:
外部密钥系统
├─ AWS Secrets Manager
├─ Google Secret Manager
├─ Azure Key Vault
├─ Vault
└─ 其他 provider
│
▼
External Secrets Controller
│ 创建或更新
▼
Kubernetes Secret
│
▼
Pod
5.1 三类对象及其职责
以 ESO 为例,常见对象包括:
SecretStore:定义某个命名空间内如何访问外部系统;ClusterSecretStore:集群范围复用的外部系统连接定义;ExternalSecret:声明从外部系统读取哪些键,以及生成哪个 Kubernetes Secret。
一个简化示例:
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: payments-store
namespace: payments
spec:
provider:
aws:
service: SecretsManager
region: ap-southeast-1
auth:
jwt:
serviceAccountRef:
name: eso-reader
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: payments
spec:
refreshInterval: 1h
secretStoreRef:
name: payments-store
kind: SecretStore
target:
name: db-credentials
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: prod/payments/database
property: username
- secretKey: password
remoteRef:
key: prod/payments/database
property: password
这段配置的含义是:
- 控制器使用
payments-store定义的身份访问外部 Secrets Manager; - 读取
prod/payments/database; - 取出其中的
username和password属性; - 创建或维护命名空间
payments中名为db-credentials的 Kubernetes Secret; - 每隔一段时间检查外部值是否变化。
apiVersion、provider 字段和认证方式受 ESO 版本影响。部署前必须检查目标 ESO 版本支持的 CRD。不能只安装 CRD 而不安装控制器,也不能假设 Kubernetes 自带这些资源。
5.2 External Secrets 并没有绕过 etcd
External Secrets 通常会创建普通 Kubernetes Secret。因此数据仍然可能经历:
外部系统明文
│ 控制器读取
▼
控制器内存
│ API 写入
▼
Kubernetes Secret
│
▼
etcd 中的密文或明文
如果没有启用 etcd 静态加密,同步到 Kubernetes 后仍可能以未加密形式保存在 etcd 中。即使启用了 KMS,Secret 仍会出现在:
- API Server 的处理路径;
- 控制器的内存;
- Kubelet 的缓存和临时卷;
- Pod 进程;
- 应用日志和错误报告。
因此 External Secrets 和 etcd 加密是互补关系:
- External Secrets 管理“来源和同步”;
- etcd 加密管理“API 存储中的静态保护”;
- RBAC 管理“谁能通过 API 读取”;
- 应用和节点安全管理“Secret 注入后的生命周期”。
5.3 控制器的状态和错误路径
External Secrets 是控制循环,不是一次性导入命令。它通常会反复执行:
- 读取
ExternalSecret; - 解析引用的
SecretStore; - 使用控制器身份访问外部系统;
- 获取远程值;
- 计算目标 Kubernetes Secret;
- 创建或更新目标 Secret;
- 写回同步状态和事件;
- 等待下一次 refresh 或外部触发。
常见失败表现包括:
kubectl describe externalsecret db-credentials -n payments
kubectl get externalsecret db-credentials -n payments -o yaml
kubectl logs -n external-secrets deploy/external-secrets
可能看到的错误类型:
SecretStore不存在或引用类型错误;- ServiceAccount 没有访问外部系统的权限;
- 云端身份绑定错误;
- 外部键不存在;
- provider API 限流或网络不可达;
- 目标 Secret 被人工修改,控制器随后恢复为期望值;
- 目标 Secret 设置为不可变,后续同步失败;
- 控制器无法更新目标 Secret 的资源版本。
同步失败时,旧的 Kubernetes Secret 可能仍然存在。应用会继续使用旧凭据,直到旧凭据在外部系统中失效;也可能因为控制器删除并重建目标对象而导致挂载或引用短暂异常。具体行为取决于 creationPolicy、删除策略和控制器版本,必须在预生产环境验证,而不能只看 refreshInterval。
6. Secret 注入方式决定轮换能否生效
Secret 常见注入方式有两种:环境变量和卷。
6.1 环境变量是进程启动时的快照
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: db-credentials
key: password
容器启动时,Kubelet 将 Secret 值设置到容器环境中。之后 Kubernetes Secret 更新,不会自动修改已经运行进程的环境变量。
因此轮换流程通常是:
Secret 更新
│
├─ 应用读取卷文件:可能在后续读取中看到新值
│
└─ 应用读取环境变量:必须重启 Pod 才能看到新值
用 envFrom 也有相同限制。
6.2 Secret 卷通常可以最终收敛到新值
volumes:
- name: db-credentials
secret:
secretName: db-credentials
containers:
- name: app
image: example/app:1.0
volumeMounts:
- name: db-credentials
mountPath: /var/run/secrets/db
readOnly: true
Kubelet 会通过缓存和同步机制更新 Secret 卷的内容。更新不是强同步事务,应用可能需要等待一段时间。应用如果只在启动时读取文件,仍然不会自动使用新凭据。
subPath 是一个重要反例:
volumeMounts:
- name: db-credentials
mountPath: /etc/app/password
subPath: password
通过 subPath 挂载的单个文件不会随着 Secret 卷更新而自动更新。此时即使 Secret 和普通卷都已更新,应用看到的文件仍可能是旧内容。
6.3 应用必须具备重新加载能力
即使文件已经更新,应用也可能:
- 把密码缓存到内存;
- 只在启动时建立数据库连接;
- 连接池继续使用旧凭据;
- 证书库只初始化一次;
- 不能在凭据变更时安全地重新认证。
因此“Secret 已更新”不等于“业务已经完成轮换”。需要明确:
- Secret 的新值何时写入;
- Kubelet 何时投影;
- 应用何时读取;
- 旧连接何时关闭;
- 外部服务何时撤销旧凭据;
- 失败时是否可以回滚或双凭据并行。
7. Secret 轮换:先区分谁在轮换
“轮换 Secret”可能表示三件不同的事。
7.1 轮换 Kubernetes 静态加密密钥
目标是改变 etcd 中 Secret 的保护密钥:
旧 DEK / 旧 provider
│ 重写
▼
新 DEK / 新 provider
它不改变数据库密码,也不改变应用看到的业务凭据。它解决的是静态存储保护。
7.2 轮换外部系统中的业务凭据
例如数据库密码从 old-password 改为 new-password。这需要外部系统支持安全变更。一个不安全的顺序是:
先撤销 old-password
│
▼
再写入 Kubernetes Secret
│
▼
应用最终得到 new-password
在传播延迟期间,应用会立即失效。
更稳妥的“双凭据”流程是:
1. 在数据库创建 new-password
2. 外部密钥系统同时保存 old 和 new,或保存新版本
3. 同步 Kubernetes Secret
4. 应用加载并验证 new-password
5. 确认所有实例完成切换
6. 撤销 old-password
7. 删除旧值并记录轮换完成
如果数据库只允许一个密码,必须使用维护窗口、连接排空、快速回滚或服务端兼容策略。
7.3 轮换 Secret 对象或版本引用
External Secrets 常用外部系统的版本、标签或路径作为引用。轮换时可能是:
- 原地修改同一外部键;
- 写入新版本并让控制器读取“当前版本”;
- 改变
remoteRef; - 创建新 Kubernetes Secret,再切换 Deployment;
- 使用不同 Secret 名称实现蓝绿切换。
这里的关键是定义一致性条件。设:
- :外部系统写入新凭据的时间;
- :External Secrets 控制器同步完成时间;
- :Kubelet 将新值投影到 Pod 的时间;
- :应用完成重新加载的时间;
- :旧凭据被撤销时间。
如果应用只能接受新凭据,则安全可用的轮换顺序应满足:
也就是说,旧凭据撤销必须晚于应用确认新凭据已经生效。若无法证明 ,就不应直接撤销旧凭据,而应采用双凭据或重叠有效期。
8. Secret 泄漏:加密后仍可能从哪些地方出现
etcd 加密只覆盖特定的存储路径,不能防止所有泄漏。
8.1 API 和 RBAC 泄漏
危险权限包括:
resources:
- secrets
verbs:
- get
- list
- watch
尤其应审查:
system:masters;- 集群管理员和平台运维角色;
- 可读取所有命名空间 Secret 的
ClusterRole; - 控制器使用的 ServiceAccount;
- CI/CD、GitOps 和备份系统;
- 能创建 Pod 或修改 Deployment 的主体。
审计日志应记录 Secret 读取、列表、更新和删除行为,但审计策略不应把 Secret 的完整 data 内容写入日志。审计日志本身也属于敏感资产。
8.2 Manifest、Git 和 CI/CD 泄漏
以下写法容易将凭据提交到 Git:
stringData:
password: real-production-password
即使使用 data:
data:
password: cmVhbC1wcm9kdWN0aW9uLXBhc3N3b3Jk
也没有改善,因为 Base64 可以还原。
需要重点检查:
- Git 历史,而不仅是当前分支;
- CI 构建日志;
set -x输出;- Helm 渲染结果;
- 临时目录和制品包;
- GitOps 控制器缓存;
- 代码评审机器人和聊天系统;
- Terraform state 或其他状态文件。
一旦凭据进入 Git 历史,删除当前文件不足以完成修复。应先撤销和轮换凭据,再清理历史和限制访问。
8.3 Pod、节点和进程泄漏
Secret 注入后,风险边界扩大到:
- 容器内拥有读取权限的其他进程;
- 容器调试和
kubectl exec; - 节点上具有足够权限的用户;
- Kubelet 和容器运行时;
/proc/<pid>/environ;- core dump;
- 应用错误日志;
- 诊断接口、线程转储和指标标签;
- 临时文件、交换区和节点备份。
环境变量尤其容易通过调试输出和崩溃诊断暴露。对于证书和文件型凭据,卷挂载能减少环境变量泄漏,但不能防止同一容器内的进程访问,也不能替代节点安全。
8.4 Webhook、控制器和中间件泄漏
API Server 处理 Secret 时,相关对象可能经过:
- 准入 Webhook;
- 策略控制器;
- GitOps 控制器;
- 备份控制器;
- External Secrets 控制器;
- 自定义 Operator。
Webhook 是否能看到完整对象取决于其订阅的资源和准入阶段,但设计时不能假定“Webhook 永远看不到 Secret”。应避免把完整对象打印到日志,并限制 Webhook 的服务账号、网络范围和保存策略。
9. 不可变 Secret 与轮换的冲突
Secret 支持:
apiVersion: v1
kind: Secret
metadata:
name: static-certificate
namespace: payments
immutable: true
type: Opaque
stringData:
certificate: "..."
immutable: true 的含义是 Secret 的数据不可再修改。它适合内容确实不应改变,或希望阻止意外修改、减少 Kubelet 监听压力的场景。
但它与原地轮换冲突:
- External Secrets 不能更新该对象的数据;
- 证书续期无法直接写入;
- 应用不会得到新值;
- 需要删除并重建 Secret,或者切换到新名称。
删除重建还有引用和时序风险。Deployment 是否会因 Secret 对象重建自动重启,取决于工作负载模板是否发生变化;仅删除并创建同名 Secret 不应被当作可靠的滚动发布机制。
更明确的做法是使用版本化名称:
db-credentials-v41
db-credentials-v42
然后通过 Deployment 模板、挂载路径或配置引用切换版本。这会增加发布协调成本,但轮换状态更可观察,也更容易回滚。
10. ServiceAccount Token 不应再按旧 Secret 模型设计
ServiceAccount 凭据是 Secret 治理中经常被误判的一类。
现代 Kubernetes 工作负载通常使用:
- TokenRequest API;
- Projected ServiceAccount Token;
- 明确的
audience; - 有限的过期时间;
- 自动轮换;
- 云厂商 Workload Identity 或类似联邦身份。
Projected Token 示例:
apiVersion: v1
kind: Pod
metadata:
name: api-client
namespace: payments
spec:
serviceAccountName: payments-api
containers:
- name: app
image: example/app:1.0
volumeMounts:
- name: token
mountPath: /var/run/secrets/tokens
readOnly: true
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: cloud-token
audience: sts.example.com
expirationSeconds: 3600
audience 限制令牌的预期接收方。令牌签发给服务 A,不应被服务 B 接受。expirationSeconds 控制有效期,具体允许范围受 Kubernetes 版本和 API Server 配置影响。
应用不能假设令牌文件永远不变。它应在文件更新后重新读取,而不是只在启动时加载一次。
旧模式是手动创建长期有效的 ServiceAccount Token Secret:
apiVersion: v1
kind: Secret
metadata:
name: legacy-token
annotations:
kubernetes.io/service-account.name: payments-api
type: kubernetes.io/service-account-token
这种方式会产生长期凭据,并把令牌作为 Secret 对象存储。现代集群通常应优先使用短期 projected token;如果使用云端身份联邦,则应让 Pod 通过短期 Kubernetes Token 换取云端临时凭据,而不是把长期云访问密钥放进 Secret。
这也说明一个边界:
etcd 加密不能替代短期令牌、受限 audience、最小权限和 Workload Identity。
它只能保护令牌在 Kubernetes 存储中的静态表示。
11. 故障诊断:先定位断在哪一段
当应用报告“凭据无效”时,不应立即重新生成 Secret。先区分故障发生在数据源、同步、存储、投影还是应用重载。
11.1 检查 Kubernetes 对象状态
kubectl get secret db-credentials -n payments \
-o jsonpath='{.metadata.creationTimestamp}{"\n"}{.metadata.resourceVersion}{"\n"}'
kubectl describe externalsecret db-credentials -n payments
kubectl get events -n payments --sort-by=.lastTimestamp
不要在排障命令中直接打印 .data。如果必须验证内容,应在受控终端中读取单个键,并避免复制到工单、聊天或日志。
检查 ExternalSecret 时重点看:
Ready或等价状态是否为成功;refreshTime是否前进;SecretStore是否可用;- 控制器事件是否显示权限、网络或远程键错误;
- 目标 Secret 的更新时间是否随外部值变化。
11.2 检查 Pod 看到的值
对于文件卷,检查文件修改时间和内容摘要比直接打印明文更安全:
kubectl exec -n payments deploy/payments-api -- \
sh -c 'stat /var/run/secrets/db/password && sha256sum /var/run/secrets/db/password'
如果应用使用环境变量,检查 Pod 是否在 Secret 更新后重建:
kubectl get pods -n payments -l app=payments-api \
-o custom-columns=NAME:.metadata.name,START:.status.startTime
如果使用 subPath,应优先怀疑挂载不会更新。若应用只在启动时读取文件,则需要显式重启或让应用实现热加载。
11.3 检查 API Server 和 KMS
KMS 相关故障通常首先出现在 API Server 日志中。应检查:
- KMS socket 是否存在且权限正确;
- KMS 插件是否运行;
- 外部 KMS 身份是否有效;
- 密钥是否被禁用或删除;
- provider 配置是否在所有 API Server 一致;
- 旧密钥是否仍保留到迁移完成。
不要在排障时直接删除旧密钥。静态加密轮换中的旧密钥可能仍是读取历史对象所必需的。
12. 生产环境中的取舍
12.1 只使用 Kubernetes Secret
优点是结构简单、组件少、与原生 API 集成好。缺点是:
- 凭据来源和轮换依赖 Kubernetes 侧流程;
- 运维人员可能直接接触 Secret;
- etcd、备份和 API 权限必须自己完整治理;
- 应用若不支持热加载,轮换需要发布。
适合凭据数量有限、已有成熟 API Server 加密和轮换流程的环境。
12.2 使用 External Secrets 加外部 KMS
优点是:
- 外部系统可以作为凭据事实来源;
- 云端或 Vault 的审计、版本和轮换能力可以复用;
- Kubernetes manifest 中不必保存明文凭据;
- 可以按命名空间和 ServiceAccount 限制读取范围。
代价是:
- 引入控制器、CRD、provider 和身份联邦;
- 需要处理同步延迟和控制器故障;
- 外部系统和 Kubernetes Secret 之间存在副本;
- 同步后的 Secret 仍必须保护;
- 外部凭据权限设计变得更加重要。
这不是“把 Secret 移出 Kubernetes 就没有泄漏”,而是把泄漏面从单一系统扩展成两个系统之间的同步链路。
12.3 不把 Secret 同步到 Kubernetes
某些应用可以直接使用 Workload Identity 访问外部密钥系统,或通过 CSI 类驱动将外部值挂载到文件系统。这样可以减少 Kubernetes Secret 对象和 etcd 副本,但仍要考虑:
- 节点和 Pod 是否能读取挂载文件;
- 驱动是否会创建同步的 Kubernetes Secret;
- 应用是否支持动态更新;
- 外部系统不可用时应用如何启动;
- 节点缓存和临时文件的清理;
- provider、驱动和云厂商的版本差异。
只有在应用和平台都能正确处理身份、挂载、轮换和故障时,这种架构才有意义。
13. 一份可执行的治理检查路径
可以按下面顺序建立最小可验证闭环。
第一步:确认授权边界
kubectl auth can-i get secrets \
--as=system:serviceaccount:payments:payments-api \
-n payments
kubectl auth can-i list secrets \
--as=system:serviceaccount:payments:payments-api \
-n payments
如果应用只需要使用挂载后的文件,通常不应授予它直接读取 Kubernetes Secret 的 API 权限。Kubelet 根据 Pod 规范完成挂载,不要求应用 ServiceAccount 直接拥有 get secrets。
第二步:确认 Secret 没有以明文进入仓库
git grep -n -i -E 'password|token|private.?key|client.?secret' -- \
'*.yaml' '*.yml' '*.json' '*.env'
这只是初筛,不能替代历史扫描。扫描结果也可能包含误报,不能因为扫描通过就认为凭据安全。
第三步:确认 etcd 加密配置和迁移状态
检查 API Server 启动参数、配置文件来源、所有控制平面实例的配置一致性,并在受控环境中验证测试对象在 etcd 中的存储状态。确认轮换时遵循:
新 provider 第一位
▼
保留旧 provider 读取
▼
分批重写所有对象
▼
验证旧格式清空
▼
删除旧 provider 和 identity
第四步:确认轮换传播路径
为应用记录以下可观察信号:
- 外部系统新凭据版本;
- ExternalSecret 最近同步时间;
- Kubernetes Secret
resourceVersion或更新时间; - Pod 文件的修改时间;
- 应用重新加载时间;
- 新旧凭据认证成功率;
- 旧凭据撤销时间。
没有这些信号时,轮换失败往往只表现为“某些 Pod 偶尔认证失败”,很难判断是同步延迟、缓存、连接池还是应用未重载。
第五步:做故障演练
至少验证以下场景:
- KMS 暂时不可用;
- 外部密钥系统返回权限错误;
- External Secrets 控制器重启;
- Secret 更新但应用不重启;
- 应用使用
subPath挂载; - 旧业务凭据被提前撤销;
- 从 etcd 备份恢复到新集群;
- API Server 高可用实例配置不一致。
故障演练的目标不是证明系统永远不会失败,而是确认失败时不会静默使用错误凭据、不会把明文写进日志,并且能够恢复到已知状态。
结语
Kubernetes Secret 治理可以分解为四个不同问题:
- 存储保护:Secret 在 etcd、备份和磁盘上是否加密;
- 密钥保护:用于加密 Secret 的密钥是否与 etcd 分离,并能可靠轮换;
- 来源与分发:凭据是否由外部密钥系统管理,如何同步到 Kubernetes;
- 生命周期与泄漏:凭据如何注入、重新加载、轮换、撤销和审计。
最容易出现的错误,是把其中一个问题当成全部安全边界:
- Base64 不等于加密;
- TLS 不等于 etcd 静态加密;
- etcd 加密不等于 API 访问控制;
- External Secrets 不等于凭据不进入 Kubernetes;
- Secret 更新不等于应用已使用新值;
- projected ServiceAccount Token 不等于长期 Token Secret;
- 删除 Git 中的文件不等于泄漏凭据已经失效。
一个可验证的安全闭环应当是:使用最小 RBAC,启用并验证 etcd 静态加密,优先使用受控 KMS,按需采用 External Secrets,设计有重叠窗口的凭据轮换流程,让应用支持安全重载,并对 API、控制器、节点、日志、备份和外部密钥系统同时进行泄漏审查。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:Kubernetes 运行时安全:非 root、Capabilities、Seccomp、AppArmor 和只读根
- 下一篇:Kubernetes Admission Control:内置插件、Webhook、策略、失败和可用性
- 延伸:ConfigMap 与 Secret:注入、更新、不可变、轮换和安全边界
- 延伸:Kubernetes ServiceAccount:Projected Token、Audience、轮换和 Workload Identity
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论