bigywater.tech
Kubernetes · 25

ServiceAccount 令牌:projected 令牌与过期

Pod 里 /var/run/secrets/kubernetes.io/serviceaccount/token 到底是什么?它是怎么来的、什么时候换、和以前那种永不过期的 Secret 令牌有什么区别?这篇把令牌 JWT 解开,逐个声明读一遍,再观察 kubelet 的刷新,并验证 automount 的优先级。

365 天 vs 3607 秒
默认受众令牌的 exp - iat,与卷里声明的 expirationSeconds(kubelet 以后者为准刷新)
11 秒
删除旧式 Secret 令牌的 Secret 之后,令牌从第几秒起返回 401
83%
600 秒有效期的令牌被 kubelet 刷新时的年龄占比(第 497 秒)

Pod 里的令牌是什么

实测 · 本机执行输出
  kube-apiserver 启动参数里与 ServiceAccount 有关的:['--service-account-issuer=https://kubernetes.default.svc.cluster.local', '--service-account-key-file=/etc/kubernetes/pki/sa.pub', '--service-account-signing-key-file=/etc/kubernetes/pki/sa.key'](没有设置 --service-account-extend-token-expiration 和 --service-account-max-token-expiration,用默认值)
  卷里的文件:total 4 | drwxrwxrwt    3 root     root           140 Oct 10 09:28 . | drwxr-xr-x    3 root     root          4096 Oct 10 09:28 .. | drwxr-xr-x    2 root     root           100 Oct 10 09:28 ..2026_10_10_09_28_08.3498460151 | lrwxrwxrwx    1 root     root            32 Oct 10 09:28 ..data -> ..2026_10_10_09_28_08.3498460151 | lrw
  claims:iss=https://kubernetes.default.svc.cluster.local;aud=['https://kubernetes.default.svc.cluster.local'];sub=system:serviceaccount:k19:default
    iat→exp 有效期 31536000 秒(365 天),而卷里声明的 expirationSeconds=3607(kubelet 按它的 80% 刷新);nbf-iat=0
    kubernetes.io 里:namespace=k19;serviceaccount=default;pod=app;node=kx-main-worker2;有 jti:True
  拿这个令牌调用 SelfSubjectReview:HTTP 201;username=system:serviceaccount:k19:default;groups=['system:serviceaccounts', 'system:serviceaccounts:k19', 'system:authenticated'];extra 里的键:['authentication.kubernetes.io/credential-id', 'authentication.kubernetes.io/node-name', 'authentication.kubernetes.io/node-uid', 'authentication.kubernetes.io/pod-name', 'authentication.kubernetes.io/pod-uid']

kubelet 通过 TokenRequest 为 Pod 取一个令牌,放进自动挂载的 projected 卷。解开 JWT:iss 是集群的 issuer,aud 是 API Server 的受众,sub 是 system:serviceaccount:k19:default,kubernetes.io 里有 namespace、serviceaccount、pod、node 以及 jti。拿它调用 API,身份的 extra 里带 pod-name、pod-uid、node-name——令牌是绑定到这个 Pod 的。

卷里写的 expirationSeconds 是 3607,令牌的 exp - iat 却是 31536000 秒(365 天):这个集群的 kube-apiserver 没有设置 --service-account-extend-token-expiration,默认会延长默认受众令牌的有效期,让还没来得及升级的客户端不至于立刻失效;kubelet 仍按 3607 秒的 80% 去刷新。

旧式 Secret 令牌

实测 · 本机执行输出
  kubectl get secret -n k19(只有一个默认 SA 在用,Pod 已 Running):No resources found in k19 namespace.
  手工创建 type=kubernetes.io/service-account-token 的 Secret 后 0.0 秒,token 控制器填上了令牌;claims 的键:['iss', 'kubernetes.io/serviceaccount/namespace', 'kubernetes.io/serviceaccount/secret.name', 'kubernetes.io/serviceaccount/service-account.name', 'kubernetes.io/serviceaccount/service-account.uid', 'sub']
    有 exp 吗:False;kubernetes.io/serviceaccount/secret.name=legacy;有 Pod 或 Node 绑定吗:False
    用它调用:HTTP 201;username=system:serviceaccount:k19:default
    删掉这个 Secret 之后:同一个令牌在 9 秒内仍通过认证,第 11 秒起 HTTP 401(旧式令牌依赖对应的 Secret 存在,但认证结果有短暂缓存)

新建的 ServiceAccount 命名空间里 kubectl get secret 是空的,不再自动生成长期令牌。要的话手工创建 type: kubernetes.io/service-account-token 并带 kubernetes.io/service-account.name 注解的 Secret,token 控制器几乎立刻填上令牌:claims 里没有 exp,没有 Pod 和 Node 绑定,只有 kubernetes.io/serviceaccount/secret.name 等。它能调用 API,直到 Secret 被删——删掉之后又过了 11 秒才开始 401(认证结果有短暂缓存)。永不过期、泄漏了就一直有效,所以尽量别用。

不要令牌

实测 · 本机执行输出
  app(默认 SA,什么都没设):ca.crt namespace token
    spec.volumes 里的名字:['kube-api-access-dpqbt']
  p-sa-off(SA 上设了 automountServiceAccountToken: false):ls: /var/run/secrets/kubernetes.io/serviceaccount: No such file or directory command terminated with exit code 1
    spec.volumes 里的名字:[]
  p-pod-off(Pod 上设了 false):ls: /var/run/secrets/kubernetes.io/serviceaccount: No such file or directory command terminated with exit code 1
    spec.volumes 里的名字:[]
  p-override(SA 为 false、Pod 上设为 true):ca.crt namespace token
    spec.volumes 里的名字:['kube-api-access-j4knz']

不需要访问 API 的应用不该有令牌。automountServiceAccountToken: false 设在 ServiceAccount 或 Pod 上都能让 Pod 里没有这个目录(spec.volumes 也不再有 kube-api-access-…);两处冲突时 Pod 上的设置优先:SA 设 false、Pod 设 true,令牌还是挂了。

令牌会自己换

实测 · 本机执行输出
  Pod rot 里 projected 令牌设 expirationSeconds=600、audience=rotate-test;每 10 秒读一次文件,共观察 567 秒,令牌(按 iat 区分)出现了 2 个版本:
    第    0 秒:iat=1791624502 exp=1791625102(有效期 600 秒);..data → ..2026_10_10_09_28_22.2122519296
    第  497 秒:iat=1791624999 exp=1791625599(有效期 600 秒);..data → ..2026_10_10_09_36_39.597382194
  第二个令牌出现在第 497 秒,约为 600 秒有效期的 83%

用最短的 expirationSeconds: 600 加自定义受众观察:第 0 秒的令牌 exp - iat = 600(自定义受众不延长);第 497 秒文件里出现了新令牌,这时旧令牌年龄是有效期的 83%——kubelet 在年龄超过 80% 之后刷新。更新是原子的:卷里的 ..data 符号链接从旧的时间戳目录切到新的,token 文件始终是完整的。应用要每次使用时重新读文件(client-go 已经这样做),不要在启动时读一次就缓存一辈子。

分步演示

ServiceAccount 令牌
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / kind(Docker 里的单节点或多节点真实集群,节点是 Docker Desktop 虚拟机里的容器)/ kubectl;用空的 kubeconfig 之外的独立 kubeconfig,不碰用户自己的集群;计时用 date +%s%N / kubectl 事件时间戳,数字每次运行会有小幅抖动。三节点 kind 集群 kx-main(Kubernetes v1.37.0,kube-apiserver 没有设置令牌有效期相关的两个参数,默认值);JWT 的声明在宿主机上解码,调用 API 用 SelfSubjectReview;令牌文件用 kubectl exec 读出,卷里的符号链接用 readlink 看;旧式令牌失效时间由每秒一次的轮询得到;轮换观察用 expirationSeconds=600、audience=rotate-test,每 10 秒读一次,共 567 秒,第二个令牌出现在第 497 秒;每种情形各一次
    • 没有验证:365 天的延长是否会写入审计日志的注解;关闭 --service-account-extend-token-expiration 后的行为;令牌被延长后过期前的警告与指标(serviceaccount_stale_tokens_total);轮换的精确触发时机(kubelet 的刷新检查间隔,只观察到 83%);projected 卷里 configMap 与 downwardAPI 的更新;ServiceAccount 的 imagePullSecrets;令牌在节点失联时的使用
    京ICP备2026067502号-1京公网安备11010502063487号