Kubernetes 身份认证与 RBAC:从请求身份到子资源授权
同一份 kubeconfig 能列出 Pod,查看日志却返回 Forbidden。给 ServiceAccount 换了一个新 token,结果仍然相同。问题不在 token 是否够新:读取 Pod 对象和读取 Pod 日志是两个授权请求。
排查这类问题,需要把连接目标、证明身份和允许动作分开。kubeconfig 保存客户端连接配置;认证把凭据转换成用户身份;授权检查这个身份能否对指定资源执行指定动作。本文沿 Kubernetes v1.35.1 的实现解释这三个层次,并用真实 ServiceAccount token 验证 Namespace 和子资源的权限。
kubeconfig 的 user 名称不等于服务端身份
kubeconfig 的 cluster 包含 API Server 地址与服务端证书信任信息,user 条目包含客户端凭据或获取凭据的 exec 配置,context 把 cluster、user 和默认 Namespace 组合起来。user 条目的名字只是本地引用名:把它改成 admin,不会让 API Server 把请求当成管理员。
API Server 根据实际提交的凭据认证。例如客户端证书先经过证书链验证,随后 CommonNameUserConversion 将证书的 Common Name 转成用户名,将 Organization 转成用户组。这个映射来自本版本源码;本文没有执行客户端证书签发实验。普通用户也不需要先创建一个 Kubernetes User 对象,证书认证得到的身份就可以成为 RoleBinding 的 subject。
ServiceAccount 是 API 对象。它的用户名遵循 system:serviceaccount:<namespace>:<name>,并带有 ServiceAccount 相关用户组。现代 Pod 通常通过投射卷使用有期限、受 audience 和对象绑定条件约束的 token;挂载 token 只提供证明身份的材料,不自动授予业务 API 权限。automountServiceAccountToken: false 可以阻止自动挂载,但不会删除 ServiceAccount,也不会撤销此前另外签发的全部 token。
签名正确还不够,ServiceAccount token 的验证入口还需检查有效期,随后检查 ServiceAccount 和绑定对象的 UID、删除状态。不能从 JWT 能被本地解码,就推断它一定能通过 API Server 认证。
同名重建的 ServiceAccount 不能沿用旧 UID 的凭据;绑定到 Pod 的 token 还需核对 Pod 的 UID 和删除时间。删除检查包含代码中的时间容差,不等于收到删除请求的同一瞬间所有 token 都立即失效。
认证成功后,请求才携带可授权的身份
认证过滤器 调用 AuthenticateRequest。失败时进入失败处理器;成功时检查 audience,并通过 WithUser 把认证结果放入请求上下文。后面的授权器读取的是这份身份信息。
因此,Unauthorized 通常提示凭据认证失败,Forbidden 则要继续看请求身份和被拒绝的动作。这里不能把 HTTP 状态码当作唯一根因:启用匿名认证时,没有凭据的请求可能被识别为 system:anonymous,然后在授权阶段得到 Forbidden;通过授权后,准入插件也可以返回 Forbidden。完整错误中的用户名、资源、子资源、动词和 Namespace 才是下一步的依据。
sequenceDiagram
participant C as kubectl
participant A as API Server 认证过滤器
participant R as 授权链中的 RBAC
participant H as 资源处理器
C->>A: 请求和客户端凭据
A->>A: 验证凭据并生成 user.Info
A->>R: 请求属性和身份
R->>R: 查找绑定并匹配规则
R-->>A: Allow 或 NoOpinion
A->>H: 授权链允许后继续
H-->>C: 资源响应
图画的是认证与授权成功路径,省略了准入和存储细节。RBAC 自己未找到允许规则时返回 NoOpinion,最终是否允许由整个授权链决定,不能把“RBAC 没有允许”画成所有集群都固定执行的一条直接拒绝指令。
Role 定义规则,Binding 决定谁在什么范围使用它
Role 保存一个 Namespace 内的规则;ClusterRole 是集群范围的规则定义,可以表达 Namespace 资源、集群资源和非资源 URL 的权限。只有角色定义,没有关联身份的 Binding,请求者不会因此得到权限。
RoleBinding 在自己的 Namespace 内生效,可以引用同 Namespace 的 Role,也可以引用 ClusterRole。引用 ClusterRole 不会把这个 RoleBinding 变成集群级授权:例如在 team-a 内绑定一个允许读 Pod 的 ClusterRole,仍然只授予 team-a 内的对应权限。ClusterRoleBinding 则引用 ClusterRole,并在集群范围授予相应规则。
这一点可以从 DefaultRuleResolver.VisitRulesFor 直接看出来:它先找适用于身份的 ClusterRoleBinding;当请求带有 Namespace 时,再查这个 Namespace 内的 RoleBinding,解析 roleRef 得到规则。请求某个 Namespace 的 Pod,不会遍历其他 Namespace 的 RoleBinding 来寻找额外授权。
RBAC 的规则是累加的。authorizingVisitor.visit 一旦遇到匹配规则,就记录 allowed 并结束继续访问。这也解释了为什么新增一个“空 Role”不能抵消已有的宽泛权限:RBAC 没有通过另一条角色规则覆盖允许结果的 deny 语义。
RoleBinding 的 roleRef 也有更新约束:ValidateRoleBindingUpdate 拒绝更改它,ClusterRoleBinding 的校验同样如此。调整 subjects 与更换所引用角色是不同权限动作;需要更换角色时应重建 Binding,并考虑删除与重建期间的权限窗口,而不是用 patch 绕过不可变字段。
pods、pods/log 与 pods/exec 匹配的是不同资源
RuleAllows 有一段很小但关键的组合逻辑:
后续分别匹配动词、API group、组合后的资源和可选资源名。读取 Pod 本体的 get pods 不会自动覆盖 get pods/log。常见 exec 客户端通过 POST 访问 pods/exec,需要相应的 create 权限;使用其他连接方式的客户端还需按实际 HTTP 请求检查动词,不能只凭终端命令名字判断。
核心 API group 在规则中写空字符串 "";Deployment 的 API group 是 apps。把资源名写对、group 写错,同样匹配不到。普通 list、get、watch 也分别授权,能够读取一个已知名字的 Pod,不代表能够 watch 全 Namespace。
实验:同一个 token 的能力随着角色规则变化
实验在 zj 的隔离 kind 集群 article-api 执行,kind v0.33.0,kubectl 与服务端均为 v1.35.1,源码和服务端提交为 8fea90b45245ef5c8ba54e7ae044d3e777c22500。配置、脚本、证据和复现说明见 k8sdev/article-api-rbac。先按该目录 README 的固定提交准备步骤创建独立工作目录,再运行实验。
权限实验使用同一 token 发起实际请求,并在修改 Role 后重新检查日志子资源权限。
实验创建 article-rbac:reader ServiceAccount,通过 TokenRequest 签发 token,并构造不含管理员客户端证书的独立客户端配置。这一步很重要:如果测试命令仍携带管理员证书,再加一个待测 token,实际认证路径可能使测试无法代表预期身份。
Role 最初只允许本 Namespace 的 get/list pods。实际请求得到:
| |
这些拒绝使用同一个有效 token,错误里识别出的用户为 system:serviceaccount:article-rbac:reader。因此它们证明的是动作或作用域不足,不是身份认证失败。
随后给 Role 增加 get pods/log,使用原 token 的权限检查由 no 变成 yes。检查调用明确使用 kubectl auth can-i get pods --subresource=log;把 pods/log 随手写成资源参数,可能被客户端当成“名为 log 的 Pod”,检查了错误对象。权限检查通过只证明授权判断,不代表容器已启动、日志文件存在或 kubelet 链路正常。
最后换成一个非法 token,移除其他认证材料,实际请求返回:
| |
auth can-i 使用访问审查 API 判断权限,不会替请求创建真实资源,也不经过待创建资源的全部准入条件。检查显示 yes 后仍然创建失败,应继续读实际错误中的 quota、安全策略或 Webhook 信息。准入控制文章接着讨论这一层。
用失败请求反推最小权限
能够创建 Role 或 RoleBinding,不等于能够给自己任意提权。Role 的存储入口会检查请求者是否已拥有拟授予的权限;缺少这些权限时,需要对该角色资源有显式 escalate 授权。Binding 的入口则检查是否有目标角色的 bind 授权,否则仍需确认请求者已拥有该角色包含的权限。常规授权通过后还存在这些防提权检查,不能只授予 create roles 就推断用户可以自建管理员角色。特权身份和显式授权的例外见下列入口,本文没有为这些新增分支补做运行实验。
RoleEscalationAuthorized 和 BindingAuthorized 把 escalate、bind 作为授权动词,再以目标角色资源和相应作用域构造请求属性。
最有效的排查输入是实际请求的身份和属性:使用哪份 context、API group 是什么、有没有子资源、动词是什么、Namespace 是什么。确认这些后,检查对应 Binding 的 subject 与 roleRef,再回到 Role 规则;不要先授予 cluster-admin 来判断问题是否消失。
长期权限还要考虑权限组合。例如允许创建任意 Pod,可能让请求者选择高权限 ServiceAccount 并挂载敏感资源;只限制直接读取 Secret 的权限,不足以证明这个身份无法取得 Secret。RBAC 的最小规则需要和 Pod 安全约束、可用 ServiceAccount 及 Namespace 隔离一起设计。官方 RBAC 文档列出了角色与绑定的完整语义。
本文验证了 token 认证、资源动作、Namespace 隔离和日志子资源授权检查;客户端证书、OIDC 和 exec 会话的机制解释来自对应源码或 API 语义,没有用这组实验替它们宣称运行验证。