默认挂载的serviceaccount token长期有效且不安全,kubernetes 1.21+起应改用投射卷方式获取短期token,需禁用automount并显式声明projected volume,1.25+集群中旧secret方式已失效。

默认挂载的 ServiceAccount Token 是长期有效的静态凭据,一旦泄露,攻击者就能持续冒用该身份访问集群资源。Kubernetes 1.21+ 已默认启用更安全的投射卷(Projected Volume)方式获取 Token,它提供自动轮转、绑定生命周期和节点级权限限制能力——你必须主动适配,否则在 1.25+ 集群中会直接拿不到 Token。
为什么不能继续依赖自动挂载的 Secret Token
旧方式(automountServiceAccountToken: true + 自动创建 Secret)生成的是永久有效 Token,且以明文文件形式挂载到 /var/run/secrets/kubernetes.io/serviceaccount/token。问题不止是“永不过期”:
- Kubernetes 1.25+ 集群中,
ServiceAccount不再自动生成关联的Secret,Pod 启动后该路径下文件为空或根本不存在 - 即使手动创建了
Secret,其 Token 仍无法被 Kubelet 轮转,也无法触发 BoundServiceAccountTokenVolume 特性带来的安全增强 - 该 Token 不受
NodeRestriction准入控制影响,也不支持节点范围权限限定(如rbac.kubernetes.io/restricted-by-node: "true")
如何启用投射卷方式获取短期 Token
核心是关闭自动挂载,并显式声明 projected 类型的 volume,让 Kubelet 通过 TokenRequest API 动态签发。关键点不是“怎么写 YAML”,而是“哪些字段不能少”:
- 必须设置
automountServiceAccountToken: false在spec级别,否则 Kubelet 仍会尝试挂载旧 Secret -
volume必须使用projected类型,并包含serviceAccountToken子项 -
serviceAccountToken.path必须与容器内程序读取路径一致(如/var/run/secrets/tokens/token) -
serviceAccountToken.expirationSeconds可设为 3600(默认值),但不能大于 86400(24 小时),否则 API Server 拒绝请求
示例片段:
spec:
automountServiceAccountToken: false
volumes:
- name: sa-token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600
audience: api
Token 绑定节点权限需要什么前提
想让 Token 的权限仅限于 Pod 所在节点(比如只允许读本节点上的 Pod、Node、Secret),光靠投射卷还不够,必须满足三个条件:
- Kubernetes 版本 ≥ 1.28(该特性在此版本正式 GA)
-
ServiceAccount对象上需添加注解:rbac.kubernetes.io/restricted-by-node: "true" - Pod 必须使用投射卷方式挂载 Token(即不能回退到 Secret 模式)
注意:此限制只对通过该 Token 发起的 API 请求生效,不改变 RoleBinding/ClusterRoleBinding 本身的定义范围;它是在鉴权阶段由 NodeAuthorizer 插件追加的额外约束。
客户端 SDK 兼容性容易被忽略
很多旧版 Kubernetes 客户端(尤其是非官方 SDK)不识别投射卷 Token 的自动刷新机制,会缓存首次读取的 Token 并长期复用。结果就是:Token 过期后请求开始 401,但程序毫无感知。
- C# 客户端需 ≥ 7.0.5 版本才支持自动重载
/var/run/secrets/.../token文件变化 - Python
kubernetes-client≥ 26.1.0 支持类似行为,但需确认是否启用了refresh_token逻辑 - 自行封装 HTTP 客户端时,务必每次请求前重新读取 Token 文件内容,而不是初始化时读一次就缓存
最稳妥的做法是:在容器启动脚本里加一个 inotifywait 监听 token 文件变更,或直接用 curl -sL /var/run/secrets/.../token 动态注入请求头——别信任何“初始化加载一次”的假设。











