Pod 里 /var/run/secrets/kubernetes.io/serviceaccount/token 到底是什么?它是怎么来的、什么时候换、和以前那种永不过期的 Secret 令牌有什么区别?这篇把令牌 JWT 解开,逐个声明读一遍,再观察 kubelet 的刷新,并验证 automount 的优先级。
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% 去刷新。
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 已经这样做),不要在启动时读一次就缓存一辈子。