Kubernetes 基础体系 · 第 45/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes ServiceAccount:Projected Token、Audience、轮换和 Workload Identity

ServiceAccount(以下简称 SA)是 Kubernetes 为 Pod 中运行的工作负载提供身份 的对象。它不是用户账号,也不是一组权限本身,而是一个可被认证系统识别的身份载体。

一个典型的 SA 身份可以表示为:

system:serviceaccount:<namespace>:<serviceaccount-name>

例如:

system:serviceaccount:payments:checkout

这个身份经过 Kubernetes API Server 认证后,还必须通过 RBAC 等授权机制检查,才能执行具体操作。因此:

ServiceAccount ≠ 权限
ServiceAccount + Token ≠ 自动拥有管理员权限

更准确的关系是:

Token 证明“请求来自哪个 ServiceAccount”
RBAC 决定“这个 ServiceAccount 能做什么”

本文重点解释现代 Kubernetes 中的短期、可轮换 ServiceAccount Token,也就是 Projected ServiceAccount Token,以及它如何通过 audience 区分资源服务器,最后说明它如何成为云厂商 Workload Identity 的基础。


一、先区分四个对象:身份、Token、认证和授权

1. ServiceAccount 是 Kubernetes 对象

ServiceAccount 是命名空间范围内的对象:

apiVersion: v1
kind: ServiceAccount
metadata:
  name: checkout
  namespace: payments

它本身通常只包含名称、UID 和少量元数据。它不保存“密码”,也不直接授予 API 权限。

Pod 可以通过以下方式指定使用哪个 ServiceAccount:

spec:
  serviceAccountName: checkout

如果没有显式设置,Pod 通常使用该命名空间的 default ServiceAccount。生产环境不应依赖这个默认行为,因为多个工作负载共用 default 会扩大身份和权限边界。


2. Token 是身份声明的可携带凭证

现代 Kubernetes 通常使用 JWT 形式的 ServiceAccount Token。JWT 由三部分组成:

base64url(header).base64url(payload).base64url(signature)

Payload 中常见的身份信息包括:

{
  "iss": "https://kubernetes.default.svc",
  "sub": "system:serviceaccount:payments:checkout",
  "aud": [
    "https://kubernetes.default.svc"
  ],
  "exp": 1710003600,
  "iat": 1709996400
}

字段含义如下:

  • iss:Issuer,签发者,表示哪个身份系统签发了 Token。
  • sub:Subject,主体身份,通常是 ServiceAccount 的完整名称。
  • aud:Audience,受众,表示该 Token 预期交给哪些资源服务器验证。
  • iat:签发时间。
  • exp:过期时间。

JWT 的签名只能证明“Token 没有被篡改,并且由对应签发者的密钥签发”。它不能单独证明调用者可以执行某个 Kubernetes API 操作。API Server 认证通过后,还要进入授权阶段。


3. 认证与授权是两个独立阶段

一次请求的抽象流程如下:

客户端携带 Bearer Token
        │
        ▼
API Server 验证 Token 签名、Issuer、Audience、有效期
        │
        ▼
得到身份:
system:serviceaccount:payments:checkout
        │
        ▼
RBAC Authorizer 检查该身份是否允许请求动作
        │
        ▼
允许或拒绝

认证失败通常返回 401 Unauthorized,例如 Token 过期、签名不匹配或 Audience 不正确。

授权失败通常返回 403 Forbidden,例如 Token 身份有效,但该身份没有 get pods 权限。

这一区别是诊断 ServiceAccount 问题的第一条规则:

401:先查 Token 和认证配置
403:先查 Role、ClusterRole 和 RoleBinding

二、Projected ServiceAccount Token 解决了什么问题

早期 Kubernetes 经常把 ServiceAccount Token 放入一个 Secret,再将 Secret 挂载到 Pod。这种 Token 通常生命周期很长,甚至可能一直有效到手工删除。它具有几个明显风险:

  1. Token 长期存在,泄漏后的影响时间很长。
  2. Token 存储在 Secret 中,通常还会进入 etcd。
  3. 任何获得该 Secret 读取权限的主体都可能获得 Bearer Token。
  4. Token 与 Pod 生命周期没有天然绑定关系。
  5. 轮换依赖人工或额外控制器。

现代 Kubernetes 使用 TokenRequest API 生成短期 Token,并通过 projected volume 将 Token 放入 Pod。

Projected volume 是一种由 kubelet 组合多个数据源的卷类型。ServiceAccount Token 只是其中一个数据源,还可以同时投影:

  • ConfigMap;
  • Downward API;
  • Secret;
  • ServiceAccount Token。

其核心变化是:

Pod 创建
  │
  ▼
kubelet 调用 TokenRequest API
  │
  ▼
API Server 签发短期 Token
  │
  ▼
kubelet 将 Token 写入 Pod 文件系统
  │
  ▼
接近过期时重新请求 Token
  │
  ▼
原子更新文件引用

因此,Token 不需要先生成一个长期保存的 Secret,也不要求应用自己向 API Server 请求 Token。

TokenRequest API 在 Kubernetes v1.20 已稳定,BoundServiceAccountTokenVolume 在 Kubernetes v1.22 稳定。当前 Kubernetes 的默认工作方式通常已经是短期、投影、可轮换的 Token。具体集群仍可能被旧配置、旧组件或云厂商发行版行为影响,不能只根据客户端版本推断服务端行为。


三、一个可运行的 Projected Token 示例

下面创建一个只允许读取 payments 命名空间中 Pod 的 ServiceAccount。

1. 创建 ServiceAccount 和 RBAC

apiVersion: v1
kind: Namespace
metadata:
  name: payments
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: checkout
  namespace: payments
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: checkout-read-pods
  namespace: payments
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: checkout-read-pods
  namespace: payments
subjects:
  - kind: ServiceAccount
    name: checkout
    namespace: payments
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: checkout-read-pods

应用:

kubectl apply -f rbac.yaml

这里的 RoleBinding 位于 payments 命名空间,因此它授予的是该命名空间内的权限,而不是整个集群权限。


2. 显式投影 Token

apiVersion: v1
kind: Pod
metadata:
  name: checkout-debug
  namespace: payments
spec:
  serviceAccountName: checkout
  automountServiceAccountToken: false

  containers:
    - name: debug
      image: curlimages/curl:8.10.1
      command: ["/bin/sh", "-c"]
      args:
        - |
          while true; do
            echo "token file:"
            cat /var/run/tokens/kube-api/token | cut -d. -f2 | base64 -d 2>/dev/null || true
            sleep 60
          done
      volumeMounts:
        - name: projected-tokens
          mountPath: /var/run/tokens
          readOnly: true

  volumes:
    - name: projected-tokens
      projected:
        sources:
          - serviceAccountToken:
              path: kube-api/token
              expirationSeconds: 3600
          - configMap:
              name: kube-root-ca.crt
              items:
                - key: ca.crt
                  path: ca.crt

这个示例做了三件事:

  1. automountServiceAccountToken: false 禁止 Kubernetes 自动挂载默认路径。
  2. serviceAccountToken 显式请求一个有效期约为 3600 秒的 Token。
  3. configMap 同时挂载集群 CA,供 HTTPS 客户端验证 API Server 证书。

kube-root-ca.crt 是现代 Kubernetes 通常自动创建的命名空间级 ConfigMap。如果某个集群没有该 ConfigMap,可以使用集群管理员提供的 CA 文件,或者使用集群中实际存在的 CA ConfigMap。

查看文件:

kubectl exec -n payments checkout-debug -- ls -l /var/run/tokens

预期可以看到:

ca.crt
kube-api

注意 Token 文件的内容是 JWT,不应在日志中完整打印。上例只为了观察 payload,而且生产环境不应输出 Token 的任何部分。


3. 调用 Kubernetes API

Pod 内部的 API Server 地址通常可以通过环境变量获得:

kubectl exec -n payments checkout-debug -- sh -c '
  API="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT_HTTPS}"
  TOKEN="$(cat /var/run/tokens/kube-api/token)"
  curl --silent --show-error \
    --cacert /var/run/tokens/ca.crt \
    -H "Authorization: Bearer ${TOKEN}" \
    "${API}/api/v1/namespaces/payments/pods"
'

如果 RBAC 配置正确,响应应包含 Pod 列表。

调用不允许访问的资源:

kubectl exec -n payments checkout-debug -- sh -c '
  API="https://${KUBERNETES_SERVICE_HOST}:${KUBERNETES_SERVICE_PORT_HTTPS}"
  TOKEN="$(cat /var/run/tokens/kube-api/token)"
  curl --silent --show-error \
    --cacert /var/run/tokens/ca.crt \
    -H "Authorization: Bearer ${TOKEN}" \
    "${API}/api/v1/namespaces/kube-system/secrets"
'

预期返回 403 Forbidden。原因不是 Token 无效,而是 checkout 的 Role 只允许访问 payments 命名空间中的 Pod。

如果把 Token 改成错误的 Audience,API Server 通常会返回 401 Unauthorized,因为认证阶段就失败了。


四、Audience:Token 到底给谁用

1. Audience 的形式化条件

设一个 JWT 的 Audience 集合为:

A_token

资源服务器配置并接受的 Audience 集合为:

A_server

资源服务器接受该 Token 的基本条件之一是:

A_token ∩ A_server ≠ ∅

也就是说,Token 声称自己是发给某个资源服务器的,而当前资源服务器必须认为自己属于这些受众之一。

例如:

A_token  = {"https://kubernetes.default.svc"}
A_server = {"https://kubernetes.default.svc"}

交集非空,Audience 检查可以通过。

如果:

A_token  = {"sts.amazonaws.com"}
A_server = {"https://kubernetes.default.svc"}

交集为空,Kubernetes API Server 不应接受这个 Token。

Audience 的直觉是:同一个身份系统可以签发多个用途不同的 Token,资源服务器不能只看签名和 Subject,还要确认 Token 是否确实发给自己。


2. Audience 不是权限

Audience 只回答:

“这个 Token 是给哪个资源服务器验证的?”

它不回答:

“这个身份是否可以删除 Pod?”

后一个问题仍然由 RBAC 或外部系统自己的授权策略决定。

因此下面两种错误理解都不成立:

audience=kubernetes.default.svc

不代表拥有 Kubernetes 管理员权限。

audience=sts.amazonaws.com

也不代表自动拥有 AWS 账号中的全部权限。


3. 不指定 Audience 和显式指定 Audience

在 Pod 的 projected volume 中,如果省略 audience

- serviceAccountToken:
    path: token
    expirationSeconds: 3600

kubelet 会请求适用于 Kubernetes API Server 的 Token。具体默认 Audience 与 API Server 的 --api-audiences--service-account-issuer 配置有关,不能在所有集群中硬编码为同一个字符串。

如果 Token 要交给外部资源服务器,就应该显式指定:

- serviceAccountToken:
    path: external/token
    audience: sts.amazonaws.com
    expirationSeconds: 3600

此时这个 Token 的用途是交给 Audience 为 sts.amazonaws.com 的服务验证。它通常不能直接拿来调用 Kubernetes API。

一个 Pod 可以投影多个 Token:

volumes:
  - name: tokens
    projected:
      sources:
        - serviceAccountToken:
            path: kube-api/token
            expirationSeconds: 3600
        - serviceAccountToken:
            path: cloud/token
            audience: sts.amazonaws.com
            expirationSeconds: 3600

这两个文件具有相同的 Kubernetes ServiceAccount 身份,但面向不同资源服务器。这样做比让一个“万能 Token”被多个系统接受更容易建立边界。


4. 由 TokenRequest API 直接请求 Token

也可以不创建 Pod,直接使用客户端请求:

kubectl create token checkout \
  -n payments \
  --duration=10m

该命令请求一个临时 Token,并输出到终端。不要把输出写入 Shell 历史、CI 日志或工单系统。

显式指定 Audience:

kubectl create token checkout \
  -n payments \
  --audience=sts.amazonaws.com \
  --duration=10m

查看 JWT Payload:

TOKEN="$(kubectl create token checkout -n payments --duration=10m)"
printf '%s' "$TOKEN" |
  cut -d. -f2 |
  base64 -d 2>/dev/null |
  jq .

不同 Kubernetes 版本和 API Server 配置可能限制可请求的最大有效期。客户端传入的 --duration 是请求,不是无条件保证;API Server 会根据自身配置决定实际有效期。


五、Token 轮换的生命周期

1. 轮换不是应用请求,而是 kubelet 负责

Projected ServiceAccount Token 的典型生命周期如下:

sequenceDiagram
    participant P as Pod
    participant K as kubelet
    participant A as API Server
    participant F as Pod 文件系统

    P->>K: Pod 使用 ServiceAccount 启动
    K->>A: TokenRequest(ServiceAccount, audience, duration)
    A-->>K: token + expirationTimestamp
    K->>F: 写入新 Token
    P->>F: 读取 token
    K->>A: 接近过期时再次 TokenRequest
    A-->>K: 新 token
    K->>F: 原子切换文件引用
    P->>F: 后续重新读取时获得新 token

kubelet 的轮换时机由实现控制。常见行为是:当 Token 使用时间达到其总寿命的约 80%,或者 Token 已经存在约 24 小时,kubelet 会主动请求新 Token。具体细节属于 Kubernetes 实现行为,应用不应依赖某个精确秒数。


2. 应用必须重新读取 Token

轮换的关键边界是:文件路径通常保持不变,但文件内容会被替换。

许多 kubelet 文件更新使用符号链接或原子切换机制。应用如果启动时只读取一次:

启动时读取 token
保存到内存
永久复用内存中的 token

那么 Token 过期后,请求会开始返回 401 Unauthorized

正确做法是:

  • 每次请求前重新读取文件;或
  • 由认证库检测过期时间并刷新;或
  • 监听文件变化,并重新打开文件读取内容。

不要只持有启动时打开的文件描述符并期待它自动变成新内容。应用应该在轮换后重新打开 Token 路径。

一个简单的 Shell 调用方式是:

TOKEN="$(cat /var/run/tokens/kube-api/token)"
curl -H "Authorization: Bearer ${TOKEN}" ...

这里每次执行命令都会重新读取文件。长期运行的 Go、Java、Python 或 Node.js 应用则应使用支持 ServiceAccount Token 轮换的客户端,或者自行实现安全的重新读取逻辑。


3. 轮换失败时会发生什么

如果 kubelet 无法向 API Server 请求新 Token,可能原因包括:

  • API Server 暂时不可达;
  • ServiceAccount 已被删除;
  • Pod 或节点身份状态异常;
  • API Server 拒绝请求的有效期或 Audience;
  • 节点时间严重漂移;
  • TokenRequest API 被错误禁用或代理链路异常。

已有 Token 在 exp 到达前通常仍可以使用;如果轮换一直失败,最终请求会变成 401

应用不应在收到 401 后无限高速重试,因为这可能把 API Server 或外部身份服务压垮。更合理的处理是:

  1. 重新读取 Token 文件;
  2. 重新建立 HTTP 客户端认证状态;
  3. 使用指数退避重试;
  4. 记录原因,但不记录完整 Token;
  5. 如果持续失败,触发工作负载健康检查或告警。

六、Bound Token:把 Token 与工作负载生命周期关联起来

TokenRequest 支持绑定对象。现代 projected Token 通常由 kubelet 代表 Pod 请求,Token 可以携带与 Pod 或其他对象相关的绑定信息。

绑定的安全意义是:

Token 不再只是“某个 ServiceAccount 的长期凭证”
而是“某个 ServiceAccount 在某个工作负载上下文中的短期凭证”

当绑定对象删除后,API Server 可以拒绝继续使用与该对象绑定的 Token。这样即使 Token 内容被复制出来,也不能像早期长期 Secret Token 那样无限期存活。

不过需要区分两件事:

  1. 短期过期:由 exp 控制。
  2. 绑定对象删除后的失效:由 Kubernetes 对绑定关系的处理控制。

绑定机制降低了泄漏后的持续时间和复用范围,但不能把 Bearer Token 变成不可复制的硬件凭证。在 Token 有效期间,拿到 Token 的攻击者仍可能以该身份使用它。


七、Legacy Secret Token 与 Projected Token 的区别

历史上,ServiceAccount 可以关联一个类型为 kubernetes.io/service-account-token 的 Secret:

apiVersion: v1
kind: Secret
metadata:
  name: checkout-token
  namespace: payments
  annotations:
    kubernetes.io/service-account.name: checkout
type: kubernetes.io/service-account-token

ServiceAccount Token Controller 会向这类 Secret 填充 Token 和 CA 等数据。

这种长期 Token Secret 已经不是推荐方式。现代 Kubernetes 默认不再为每个 ServiceAccount 自动创建长期 Token Secret;在 Kubernetes v1.24 之后尤其需要注意这一行为变化。kubectl get secrets 看不到某个 ServiceAccount 的 Token,并不表示 projected Token 不存在。

对比如下:

特征 Projected Token Legacy Secret Token
获取方式 TokenRequest API Secret Controller
生命周期 短期、可轮换 通常长期
是否绑定 Pod 可以绑定 通常不绑定具体 Pod
是否作为 Secret 对象持久化 通常不需要
泄漏后的暴露窗口 相对较短 可能长期有效
推荐程度 当前推荐 仅在兼容旧系统时谨慎使用

如果旧系统只能读取 Secret,可以手工创建兼容的 Token Secret,但应把它当作高风险例外,并建立明确的轮换和撤销流程。不能因为 Token 放在 Kubernetes Secret 中,就误以为它自动安全;Secret 仍可能被 API 读取、备份到 etcd、导出到日志或复制到其他系统。


八、Workload Identity:从 Kubernetes 身份换取外部身份

1. Workload Identity 的定义

Workload Identity 是一种身份联邦模式:

Pod 使用 Kubernetes ServiceAccount 身份
        │
        ▼
获得一个带指定 Audience 的短期 JWT
        │
        ▼
外部身份服务验证该 JWT
        │
        ▼
将 Kubernetes 身份映射到云平台或外部系统身份
        │
        ▼
换取外部访问 Token 或临时凭证

关键点是:外部系统不直接信任 Pod 内的一段任意字符串,而是验证 Kubernetes 签发的 JWT。

外部验证者通常至少检查:

签名是否正确
Issuer 是否是预期的 Kubernetes OIDC Issuer
Audience 是否是外部系统自己
exp 是否尚未过期
sub 是否映射到允许的工作负载

令牌验证通过后,外部系统还要执行自己的授权策略。例如将:

system:serviceaccount:payments:checkout

映射到一个云角色:

cloud-role/payments-checkout

这里的映射规则必须足够精确,不能只允许:

sub startsWith "system:serviceaccount:"

否则同一集群中大量 ServiceAccount 可能获得不应有的云权限。


2. 为什么 Audience 对 Workload Identity 必不可少

假设 Kubernetes API Token 的 Audience 是:

https://kubernetes.default.svc

云 STS(Security Token Service)不应接受它,因为它不是发给 STS 的。

Workload Identity 通常会请求:

- serviceAccountToken:
    path: cloud/token
    audience: <外部身份服务要求的 audience>
    expirationSeconds: 3600

于是 JWT 中的 Audience 变成外部身份服务认可的值。

形式化地说,外部身份服务 STS 接受 Token 的必要条件之一是:

aud_token ∈ Audiences_accepted_by_STS

而 Kubernetes API Server 需要满足:

aud_token ∈ Audiences_accepted_by_Kubernetes_API_Server

如果这两个集合没有交集,那么同一个 Token 不能同时满足两套检查。这正是多 Audience 设计的安全价值:减少 Token 跨系统复用。


3. 外部交换流程

以通用 STS 为例,流程可以表示为:

sequenceDiagram
    participant W as Pod 工作负载
    participant K as kubelet
    participant A as Kubernetes API Server
    participant S as 外部 STS
    participant R as 云资源

    W->>K: 请求读取 projected token
    K->>A: TokenRequest(audience=STS)
    A-->>K: 短期 Kubernetes JWT
    K-->>W: 写入 token 文件
    W->>S: 提交 Kubernetes JWT
    S->>S: 验证签名、iss、aud、exp、sub
    S-->>W: 临时云 Token/凭证
    W->>R: 使用临时凭证访问资源

外部系统通常不需要读取 Kubernetes API,也不需要知道 Pod 的网络地址。它只需要:

  1. 能发现 Kubernetes OIDC Issuer;
  2. 能获取并信任对应的签名公钥;
  3. 配置允许的 Issuer、Audience 和 Subject;
  4. 配置 Kubernetes ServiceAccount 到外部角色的映射。

九、云厂商差异不能被抽象掉

Workload Identity 是模式,不是一个由 Kubernetes 单独定义的完整 API。不同云厂商的接入方式不同,不能把某一家厂商的参数当成 Kubernetes 通用字段。

AWS

Amazon EKS 的 IRSA(IAM Roles for Service Accounts)典型模式是:

  • 集群提供 OIDC Issuer;
  • Pod 获得 Audience 为 sts.amazonaws.com 的 projected Token;
  • AWS STS 使用 AssumeRoleWithWebIdentity 交换临时凭证;
  • IAM 信任策略限制 Issuer、Audience 和 Subject。

常见的 Subject 约束类似:

system:serviceaccount:payments:checkout

AWS 还提供 EKS Pod Identity 等其他机制。它与传统 IRSA 的数据流、节点代理和凭证注入方式不同,不能简单认为“只要有 ServiceAccount Token 就一定是 IRSA”。


Google Cloud

Google Cloud Workload Identity Federation 通常通过 Google Security Token Service 接收外部 OIDC 令牌,再将其交换为 Google 访问令牌或服务账号身份。

实际配置涉及:

  • Kubernetes OIDC Issuer;
  • Workload Identity Pool;
  • Provider;
  • Audience;
  • 属性映射;
  • Google 服务账号绑定。

Google 侧的主体映射语法和权限绑定方式不是 Kubernetes RBAC 语法,不能把 RoleBinding 当作 Google IAM 授权。


Microsoft Azure

Azure Workload Identity 通常依赖:

  • Kubernetes projected Token;
  • Azure AD 的 Federated Identity Credential;
  • 指定 Issuer、Subject 和 Audience;
  • Azure SDK 使用交换后的访问令牌。

常见配置会使用类似:

api://AzureADTokenExchange

作为 Audience,但具体值由 Azure 集成方式和组件要求决定,应以对应版本的官方文档为准。Azure Workload Identity Webhook 还可能通过 Pod 标签、ServiceAccount 注解或环境变量向应用注入配置;这些不是 Kubernetes 核心 API 的统一行为。


十、OIDC Issuer 和公钥轮换

外部系统验证 Kubernetes Token,必须验证 JWT 签名。通常需要:

Issuer URL
    │
    ├── OpenID Configuration
    │       └── jwks_uri
    │
    └── JWKS 公钥集合

验证者可以通过 OIDC Discovery 找到配置,再从 JWKS 获取公钥。Kubernetes API Server 自己也使用配置的 ServiceAccount Issuer 和签名密钥来验证 Token。

生产环境需要关注签名密钥轮换:

  • 外部验证器必须定期刷新 JWKS;
  • 验证器应支持同时信任旧公钥和新公钥的过渡窗口;
  • 不能只在启动时拉取一次公钥;
  • Issuer URL 必须稳定,且 TLS 证书链应被验证;
  • 不应为了调试而关闭签名或 TLS 校验。

如果 Token 签发者更换了 Issuer,所有依赖旧 Issuer 的外部信任关系都可能失败。Issuer 不是普通标签,而是身份系统的根边界之一。


十一、常见失败表现和诊断路径

1. Pod 中没有 Token 文件

检查:

kubectl get pod checkout-debug -n payments \
  -o jsonpath='{.spec.serviceAccountName}{"\n"}'

kubectl get pod checkout-debug -n payments \
  -o jsonpath='{.spec.automountServiceAccountToken}{"\n"}'

重点检查:

  • Pod 是否使用了预期的 ServiceAccount;
  • 是否设置了 automountServiceAccountToken: false
  • 是否正确挂载了 projected volume;
  • volumeMount 路径是否与应用读取路径一致;
  • Pod 是否已重新创建,旧 Pod 不会自动采用修改后的 Pod 模板。

查看事件:

kubectl describe pod checkout-debug -n payments

如果是 kubelet 无法完成 TokenRequest,通常可以在节点 kubelet 日志和 API Server 审计日志中找到线索。


2. API Server 返回 401

典型原因包括:

  • Token 已过期;
  • 应用缓存了旧 Token;
  • Audience 不匹配;
  • Issuer 不匹配;
  • 签名密钥或 JWKS 不匹配;
  • 节点或 API Server 时间不准确;
  • Token 被绑定对象删除后不再有效。

检查 JWT 的非敏感元数据:

kubectl exec -n payments checkout-debug -- sh -c '
  cut -d. -f2 /var/run/tokens/kube-api/token |
  base64 -d 2>/dev/null
'

不要把完整 JWT 发送到聊天工具或日志平台。Payload 也可能包含足以识别工作负载的身份信息,应按敏感信息处理。

可以先确认文件是否发生变化:

kubectl exec -n payments checkout-debug -- sh -c '
  sha256sum /var/run/tokens/kube-api/token
'

间隔一段时间再次执行。如果 kubelet 完成轮换,哈希通常会变化;但精确轮换时间取决于 Token 有效期和 kubelet 行为,不能用一次观察推断机制失效。


3. API Server 返回 403

这表示 Token 大概率已经通过认证,问题应转向授权:

kubectl auth can-i \
  --as=system:serviceaccount:payments:checkout \
  --namespace=payments \
  get pods

kubectl auth can-i \
  --as=system:serviceaccount:payments:checkout \
  --namespace=payments \
  get secrets

预期第一个为:

yes

第二个通常为:

no

kubectl auth can-i 使用的是本地用户或管理员权限来模拟检查,不等同于让 Pod 实际发出请求,但它适合验证 RBAC 规则是否符合预期。


4. 外部 STS 返回无效 Audience

这类错误通常不是 RBAC 问题,而是外部身份交换配置不一致。需要逐项比较:

Pod Token 的 aud
外部 Provider 配置的 audience
STS 实际接受的 audience

例如,Pod 配置为:

audience: sts.amazonaws.com

但外部 Provider 要求:

api://AzureADTokenExchange

那么交换必然失败。不能通过给 Token 添加更多无关 Audience 来“碰运气”,因为部分验证器要求 Audience 精确匹配,且多 Audience 会扩大令牌的可接受范围。


十二、生产边界与安全取舍

1. 关闭不需要的自动挂载

如果一个 Pod 不需要调用 Kubernetes API,也不需要参与 Workload Identity,可以关闭自动挂载:

spec:
  automountServiceAccountToken: false

这样可以减少应用误读 Token、日志误打印 Token 和漏洞利用 Token 的机会。

如果只有一个容器需要 Token,优先使用显式 projected volume,并仅挂载到该容器,而不是让整个 Pod 的所有容器都看到同一凭证。


2. 不要给工作负载绑定过大的 RBAC 权限

下面的配置风险很高:

rules:
  - apiGroups: ["*"]
    resources: ["*"]
    verbs: ["*"]

原因不是 Token 机制不安全,而是 Token 一旦泄漏,攻击者得到的是这个 RBAC 身份的全部能力。

应以实际 API 调用为依据授予:

  • 精确的 API Group;
  • 精确的 Resource;
  • 精确的 Verb;
  • 尽可能小的命名空间范围。

尤其要谨慎对待:

  • secrets
  • nodes
  • pods/exec
  • pods/attach
  • serviceaccounts/token
  • rolesrolebindings
  • 集群级资源。

拥有创建或读取其他 ServiceAccount Token 的能力,可能导致身份边界被突破。


3. Short-lived 不等于无风险

短期 Token 只能降低泄漏后的有效时间,不能消除以下风险:

  • 应用漏洞直接读取 Token;
  • Pod 内恶意进程窃取 Token;
  • 日志、诊断转储或错误页面输出 Token;
  • RBAC 权限本身过大;
  • 外部身份映射过宽;
  • 节点管理员可以读取 Pod 文件系统或 kubelet 数据;
  • Token 在有效期内被复制并重放。

因此,Token 生命周期、RBAC、容器隔离、节点安全和审计必须共同构成防线。


4. Projected Token 不等同于 Secret 管理

Projected Token 通常不创建 Kubernetes Secret,但它仍然是凭证。它解决的是:

短期生成、按受众签发、绑定工作负载、自动轮换

它没有自动解决:

etcd 中其他 Secret 的加密
外部 Secret 的轮换
应用日志脱敏
节点上的特权访问
云 IAM 过宽
泄漏后的审计和响应

因此,ServiceAccount Token 治理应与 Secret 治理分开设计。前者关注身份和凭证生命周期,后者还包括 etcd 加密、KMS、External Secrets、静态密钥轮换以及数据导出链路。


十三、一个完整的判断框架

当一个工作负载需要访问资源时,可以按以下顺序推导:

第一步:确定调用对象

是:

Kubernetes API Server

还是:

云 STS

还是:

自建资源服务器

不同调用对象应使用不同 Audience。


第二步:确定 Token 身份

检查 Pod 使用的 ServiceAccount:

kubectl get pod <pod> -n <namespace> \
  -o jsonpath='{.spec.serviceAccountName}{"\n"}'

预期身份为:

system:serviceaccount:<namespace>:<serviceaccount>

第三步:确定认证条件

资源服务器必须接受:

签名密钥
Issuer
Audience
有效期
必要时的绑定对象

任何一个条件不满足,都可能导致 401


第四步:确定授权条件

Kubernetes API 需要检查:

kubectl auth can-i \
  --as=system:serviceaccount:<namespace>:<serviceaccount> \
  --namespace=<namespace> \
  <verb> <resource>

外部云平台则需要检查它自己的 IAM、角色信任策略或联邦映射。


第五步:确定轮换行为

应用必须确认:

Token 文件是否会重新读取
HTTP 客户端是否缓存旧凭证
SDK 是否支持文件型凭证轮换
Token 过期后是否会退避重试

如果应用只在启动时读取一次 Token,那么它实际上依赖的是“直到过期前都不出问题”的短期凭证,轮换并没有真正被应用利用。


结语

现代 Kubernetes ServiceAccount 的核心模型可以压缩为:

ServiceAccount
  └── 代表工作负载身份

TokenRequest
  └── 按需签发短期 JWT

Projected Volume
  └── 将 Token 注入 Pod 文件系统

Audience
  └── 限定 Token 面向哪个资源服务器

kubelet Rotation
  └── 在接近过期时重新请求并替换 Token

Workload Identity
  └── 将 Kubernetes 身份联邦到外部云或身份系统

真正安全的使用方式不是“给 Pod 挂一个 Token”,而是同时建立四个边界:

身份边界:哪个 ServiceAccount
受众边界:哪个资源服务器
时间边界:Token 何时过期和轮换
权限边界:认证后允许执行什么操作

只要其中一个边界被忽略,短期 Token 仍可能因为 Audience 配置错误、RBAC 过宽、应用不轮换或外部身份映射过度而造成实际风险。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。