前言

几乎所有第一次往 Kubernetes 集群里部署业务的人,都会遇到过这样的报错:pods "xxx" is forbidden: User "system:serviceaccount:default:default" cannot get resource "pods" in the namespace "default"。这条报错信息本身就是 Kubernetes 权限体系的一次自我陈述:它告诉你「谁」(system:serviceaccount:default:default 这个 ServiceAccount)、「想做什么」(get pods)、「在哪里做」(default 命名空间),以及「为什么被拒绝」(没有任何一条授权规则允许这件事)。

Kubernetes 的访问控制是一个分层体系:先认证(Authentication),确认「你是谁」;再授权(Authorization),判断「你能做什么」;最后还有准入控制(Admission Control),在请求被允许之后对对象内容做进一步校验。本文以官方文档为主线,逐层拆解认证与授权的分工,重点带读基于角色的访问控制(RBAC)这一生产环境中最常用的授权机制,覆盖 Role、ClusterRole、RoleBinding、ClusterRoleBinding 四种对象的组合语义、最小权限设计、ServiceAccount 与令牌投射、聚合 ClusterRole、越权排查命令和多租户隔离实践。文中所有 API 名称、配置项、命令参数均以当前版本官方文档为准。

对于习惯了 Java 后端权限模型(比如 Spring Security 里的 Role、Authority、FilterChain)的读者来说,Kubernetes RBAC 的概念并不陌生,但它的「作用域」思维和「纯累加、无拒绝」的规则模型很有特点,值得系统地过一遍官方文档,而不是靠网上过时的博客片段拼直觉。

整体架构与核心概念

先建立全局图景。一次 API 请求进入 kube-apiserver 之后,会依次经过认证、授权、准入三个阶段,RBAC 只是授权阶段众多授权器(authorizer)中的一个:

                 +------------------+
   kubectl ----> |  kube-apiserver  |
   其他客户端     |                  |
                 | 1. 认证(Authentication)
                 |    - X.509 客户端证书
                 |    - Bearer Token(SA/OIDC/Webhook/静态文件)
                 |    产出: user / groups / extra
                 |         |
                 | 2. 授权(Authorization)
                 |    - Node -> RBAC -> Webhook -> ...
                 |    每个授权器返回: allow / deny / no opinion
                 |    全部 no opinion => 拒绝(HTTP 403)
                 |         |
                 | 3. 准入控制(Admission)
                 |    授权通过后才执行,可修改/拒绝对象
                 +------------------+

官方文档对授权裁决的描述非常精确:API 服务器会把请求属性与所有策略逐一比对,请求的所有部分都必须被某种授权机制允许才能放行,换句话说就是默认拒绝。多个授权模块按顺序检查,任何一个返回允许或拒绝,裁决立即生效,其余授权器不再 consulted;如果所有模块都表示「无意见」(no opinion),请求被拒绝,API 服务器返回 HTTP 403。

授权器看到的请求属性包括:user、group、extra(认证层附带的键值对)、API 与请求路径、API 请求动词(get、list、create、update、patch、watch、delete、deletecollection)、HTTP 方法(用于非资源请求)、资源名、子资源、命名空间和 API 组。也就是说,RBAC 规则里的每一个字段,都对应着这次请求的真实属性。

认证与授权的分层

官方认证文档列出的认证方式包括:X.509 客户端证书、引导令牌(Bootstrap tokens)、ServiceAccount 令牌、静态令牌文件,以及外部集成(JWT、OpenID Connect 令牌、Webhook 令牌认证、认证代理)。认证阶段只负责把凭证翻译成「用户名 + 组 + 额外字段」这组属性,这组属性对认证系统本身是不透明的,只有授权器才解释它们的含义。

几个值得记住的细节:

  • 启用多个认证器时,第一个成功认证的模块短路后续模块,API 服务器不保证认证器之间的执行顺序。
  • X.509 客户端证书认证由 --client-ca-file 开启,用户名取自证书的 commonName,组取自证书的 organization 字段。Kubernetes 不支持证书吊销,任何已签发的证书在过期前都有效——这意味着给离职员工签的客户端证书必须靠短有效期来兜底。
  • 匿名请求默认开启(只要授权模式不是 AlwaysAllow),匿名用户是 system:anonymous,组是 system:unauthenticated,可用 --anonymous-auth=false 关闭。
  • 授权模式包括 AlwaysAllow、AlwaysDeny、ABAC、RBAC、Node、Webhook。官方明确警告:AlwaysAllow 会绕过授权,不要用于不可信客户端可及的集群;--authorization-mode=AlwaysAllow,RBAC 的效果等价于只用 AlwaysAllow,因为 RBAC 不提供拒绝规则。
  • system:masters 组是硬编码的超级用户组,绕过 RBAC 和 Webhook 的一切限制。官方建议避免把人加进这个组,需要集群管理权限时应创建指向内置 cluster-admin ClusterRole 的 ClusterRoleBinding。

授权模块的启用方式也有新旧两种:新的推荐做法是通过 --authorization-config 指定一份 AuthorizationConfiguration 文件(apiserver.config.k8s.io/v1),在 authorizers 列表中加入 - type: RBAC;旧做法是 --authorization-mode=...,RBAC。两种配置方式互斥,同时设置会在启动时直接报错退出。

apiVersion: apiserver.config.k8s.io/v1
kind: AuthorizationConfiguration
authorizers:
  - type: Node
  - type: RBAC
  - type: Webhook
    # 更多 webhook 与失败策略等高级能力只有配置文件方式支持

你现在应该掌握了什么:认证回答「你是谁」,产出用户名和组;授权回答「你能不能做这件事」,默认拒绝,多个授权器按序短路裁决;RBAC 通过 rbac.authorization.k8s.io API 组把授权策略变成可以用 kubectl 管理的普通对象。

四种 API 对象:Role、ClusterRole、RoleBinding、ClusterRoleBinding

RBAC API 声明了四种对象:Role、ClusterRole、RoleBinding、ClusterRoleBinding。官方文档强调了一个常被忽略的前提:权限是纯累加的,不存在「拒绝」规则。这决定了整个 RBAC 的设计哲学——你只能授予,不能排除,因此每一条规则都应当被当作一种责任来写。

Role 和 ClusterRole 装的是规则(rules),每条规则由 apiGroups、resources、verbs(可选 resourceNames、nonResourceURLs)组成。区别在于作用域:Role 总是属于某个命名空间,创建时必须指定 namespace;ClusterRole 是集群级资源。Kubernetes 对象要么是命名空间的要么不是,不能两者兼得,所以 Role 和 ClusterRole 是两个不同的资源。

ClusterRole 有三种典型用途:在单个命名空间内定义对命名空间资源的权限(配合 RoleBinding);跨所有命名空间定义权限(配合 ClusterRoleBinding);定义对集群级资源(如 nodes)的权限。

RoleBinding 和 ClusterRoleBinding 则把「主体」(subjects:用户、组、ServiceAccount)和角色关联起来。这里有一个非常容易踩的坑,也是面试高频考点:RoleBinding 可以引用 ClusterRole,但权限仍被限制在 RoleBinding 所在的命名空间。官方文档的例子是:一个 RoleBinding 位于 development 命名空间、引用了能读全部 secrets 的 ClusterRole,那么主体 dave 只能读 development 里的 secrets。这种「RoleBinding + ClusterRole」的组合让你可以在整个集群定义一套通用角色,再在各命名空间复用,是生产环境中最常用的授权姿势。

四种对象两两组合,语义如下表:

角色类型 绑定类型 生效范围 典型场景
Role RoleBinding 单个命名空间 给某应用的 SA 读本命名空间的 ConfigMap
ClusterRole RoleBinding 单个命名空间(受绑定所在命名空间限制) 一套通用只读角色在多命名空间复用
Role ClusterRoleBinding 无效(Role 不能跨命名空间授予) ——
ClusterRole ClusterRoleBinding 整个集群 只读监控账号、CNI 插件、控制器

一个读 pods 的 Role 示例(来自官方示例 simple-role.yaml):

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: pod-reader
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "watch", "list"]

对应的 RoleBinding 把 pod-reader 授予用户 jane:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: default
subjects:
- kind: User
  name: jane
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

注意 roleRef 是不可变的:官方文档明确说,创建绑定后不能修改它引用的 Role/ClusterRole,强行改 roleRef 会得到校验错误。想换角色只能删除重建。这条限制有两个原因:一是 roleRef 不可变使得「允许某人 update 现有绑定」可以安全地只用于维护主体列表;二是绑定到不同角色本质上是另一个绑定,强制重建能保证主体列表被整体确认过。如果确实要批量修改,官方提供的工具是 kubectl auth reconcile,它能在需要改变 roleRef 时自动完成删除重建。

资源引用的细节也值得逐条读:

  • RBAC 用的资源名和 API 路径 URL 里的名字完全一致(pods、deployments.apps、configmaps),不是 Kind 名。
  • 子资源用斜杠分隔:pods/log、pods/status、pods/exec。
  • resourceNames 可以把权限收窄到具体对象实例,但官方 note 明确:deletecollection 和顶层 create 不能按名称限制(create 时授权阶段还不知道新对象的名字);如果按 resourceName 限制 list/watch,客户端必须在请求里带上匹配的 metadata.name 字段选择器。
  • 通配符 * 可以用于 apiGroups、resources、verbs;nonResourceURLs 中的 * 是后缀 glob 匹配。官方对此有明确警告:通配符会对新出现的资源、子资源、自定义动词自动放行,属于过度授权,应遵循最小权限原则,显式枚举资源和动词。
  • 非资源端点(如 /healthz、/metrics)只能出现在 ClusterRole 里,且要配合 ClusterRoleBinding 才生效。

你现在应该掌握了什么:四种对象的分工——Role/ClusterRole 定义「能做什么」,RoleBinding/ClusterRoleBinding 定义「谁能做」;RoleBinding 引用 ClusterRole 时权限被收敛到绑定所在命名空间;roleRef 不可变;规则字段与请求属性一一对应,资源名与 URL 一致,子资源用斜杠拼接。

请求动词映射与最小权限设计

要把最小权限落到实处,得先理解 kubectl 的「大白话」背后对应哪些授权动词。官方授权文档给出了一张 HTTP 动词到请求动词的映射表:

HTTP 动词 请求动词 说明
POST create 创建资源
GET / HEAD get / list / watch 单个对象为 get,集合为 list,watch 单独检查
PUT update 整体替换
PATCH patch 局部修改
DELETE delete / deletecollection 单个对象为 delete,集合为 deletecollection

官方 caution 里有一句很关键的提醒:get、list、watch 在「能返回多少数据」这件事上是等价的——对 secrets 的 list 会返回所有对象的 data 字段。所以不要以为「只给了 list 没给 get」就安全了,list 本身就可能造成敏感数据批量泄露。反过来想,这也就解释了为什么内置的 view 角色不允许读 secrets:读 secrets 等于拿到该命名空间所有 ServiceAccount 的凭据,等于可以以任何 SA 的身份访问 API,是一种提权路径。

除了标准动词,Kubernetes 还会检查一些特殊动词:impersonate(对 users、groups、serviceaccounts)、RBAC 的 escalate 和 bind(后文细讲)、CertificateSigningRequests 的 approve 等。设计角色时,动词粒度就是你权限模型的粒度。

最小权限的实践路径,官方 RBAC 最佳实践的核心建议可以归纳为:

  1. 列出工作负载真正需要的 API 交互:读什么资源、写什么资源、是否 watch、是否需要跨命名空间。
  2. 用显式的 apiGroups + resources + verbs 枚举,拒绝 * 通配符的诱惑。
  3. 用 resourceNames 进一步收窄到具体对象(配合 get/update)。
  4. 用 RoleBinding 把 ClusterRole 收敛到单个命名空间,而不是一上来就 ClusterRoleBinding。
  5. 定期用审计和 can-i 命令复核实际授予与理论需求的差距(后文有专门一节)。

举个 Java 后端读者熟悉的类比:RBAC 规则就像方法级的权限注解 @PreAuthorize("hasAuthority('configmaps:update')"),verbs 是方法名,resources 是领域对象,resourceNames 是对象主键,namespace 是租户边界。不同的是 Kubernetes 把这套注解变成了声明式对象,由 API 服务器统一执行,与你的应用进程完全解耦。

一个典型「最小权限」角色长这样——只允许更新名为 my-configmap 的那一个 ConfigMap:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: configmap-updater
rules:
- apiGroups: [""]
  resources: ["configmaps"]
  resourceNames: ["my-configmap"]
  verbs: ["update", "get"]

你现在应该掌握了什么:HTTP 方法到授权动词的映射表;list 与 get 在数据暴露上等价;设计角色时按「资源 × 动词 × 名称 × 命名空间」四个维度显式收敛;能用 resourceNames 把权限钉在具体对象上。

默认角色、自动协调与内置 ClusterRole 生态

API 服务器会创建一组默认的 ClusterRole 和 ClusterRoleBinding,它们都带标签 kubernetes.io/bootstrapping=rbac-defaults。带 system: 前缀的是控制面直接管理的,官方 caution 明确警告:随意修改它们可能导致集群不可用。

默认角色分三类。第一类是 API 发现类角色(system:basic-user、system:discovery、system:public-info-viewer),用来让已认证甚至未认证用户读到发现和协商 API 所需的基本信息;v1.14 起 system:public-info-viewer 被绑定到 system:authenticated 和 system:unauthenticated 两个组。第二类是面向用户的角色,这是我们应该重点掌握的:

默认 ClusterRole 默认绑定 语义与边界
cluster-admin system:masters 组 超级用户。配合 ClusterRoleBinding 控制全集群;配合 RoleBinding 则只控制绑定所在命名空间(包括命名空间本身)
admin 无 用 RoleBinding 授予。命名空间内大部分资源读写,可在命名空间内创建角色和绑定;不能写资源配额、命名空间本身,v1.22+ 起不含 EndpointSlices 写权限
edit 无 命名空间内大部分对象读写;不能看/改角色与绑定;但可以读 Secrets、以任意 SA 运行 Pod,因此间接获得该命名空间任意 SA 的 API 权限
view 无 只读大部分对象;不能看角色与绑定,也不能看 Secrets(防止通过 SA 凭据提权)

第三类是组件角色,如 system:kube-scheduler、system:kube-controller-manager、system:node-proxier 等。值得专门提 system:node:它给 kubelet 所需的全部权限,包括所有 secrets 的读权限和所有 pod status 的写权限,官方明确说应改用 Node 授权器加 NodeRestriction 准入插件,system:node 仅为兼容 v1.8 之前的升级集群而存在。

另一组需要认识的是 system:controller:* 系列角色。kube-controller-manager 带 --use-service-account-credentials 启动时,每个内置控制器用独立的 ServiceAccount 运行,对应独立的 system:controller:deployment-controller、system:controller:job-controller 等角色;不启用该参数时所有控制循环共用 controller manager 自己的凭据,那个凭据就要被授予所有相关角色。这组命名本身就是一份「控制器需要哪些权限」的活字典,自己写 operator 时可以对照参考。

还有一个机制叫自动协调(auto-reconciliation):每次 API 服务器启动时,会补全默认集群角色缺失的权限和默认绑定缺失的主体,这让集群能修复误删误改,并随版本升级自动跟进。想退出协调,可以给对象加注解 rbac.authorization.kubernetes.io/autoupdate: "false"——但官方同时提醒,缺失默认权限和主体可能导致集群不可用。这也意味着:你手工改默认角色,重启后会被改回去,想持久生效就得自建角色而不是改 system: 角色。

你现在应该掌握了什么:面向用户的 admin/edit/view 三个默认角色的边界(尤其 view 不读 Secrets、edit 可借 SA 提权);默认角色每次 apiserver 启动自动修复,改它不如自建;内置控制器角色是写 operator 权限需求的参考实现。

权限提升防护:escalate、bind 与引导

RBAC API 在 API 层面阻止用户通过编辑角色或绑定来提升自己,注意这个限制即使在未启用 RBAC 授权器时也生效,因为它实现在 API 层面而非授权层。

限制分两条线。第一条线:创建/更新 Role 或 ClusterRole 时,你必须满足以下二者之一——你已经拥有该角色包含的全部权限(且作用域相同,ClusterRole 对应集群级,Role 对应同一命名空间),或者你被显式授予了对 roles/clusterroles 资源的 escalate 动词。举例:user-1 没有集群级 list secrets 的权限,他就无法创建一个包含该权限的 ClusterRole。

第二条线:创建/更新 RoleBinding/ClusterRoleBinding 时,你必须已经拥有被引用角色包含的全部权限(同作用域),或者被授权对该角色执行 bind 动词。同理,user-1 不能创建指向「集群级 list secrets」角色的 ClusterRoleBinding。

官方给了一个「授权员」模式的完整示例:让 user-1 可以在 user-1-namespace 里给别人授予 admin、edit、view 角色,但别的什么都不行——

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: role-grantor
rules:
- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["rolebindings"]
  verbs: ["create"]
- apiGroups: ["rbac.authorization.k8s.io"]
  resources: ["clusterroles"]
  verbs: ["bind"]
  resourceNames: ["admin","edit","view"]  # 只许绑定这三个角色,其余拒绝
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: role-grantor-binding
  namespace: user-1-namespace
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: role-grantor
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: user-1

初次引导(bootstrapping)时,第一个用户还没有任何权限可校验,这时官方给出的出路是使用带 system:masters 组的凭据(默认绑定到了 cluster-admin),完成首批角色和绑定的创建。

这组机制对平台团队的启示很直接:如果你想做一个「自助授权平台」让每个团队自己给自己发权限,不要直接给他们 cluster-admin,而是给他们一套「bind 白名单」式的受限授权员角色,把可授予的角色集合用 resourceNames 钉死。

你现在应该掌握了什么:escalate 管「能不能造出含某权限的角色」,bind 管「能不能把某角色授出去」;两者都用 resourceNames 精确圈定范围;首次引导靠 system:masters 凭据完成。

ServiceAccount 与令牌投射

工作负载的身份由 ServiceAccount 承载。默认 RBAC 策略不给 kube-system 之外任何 ServiceAccount 授权(API 发现类权限除外),所以新 SA 默认什么都做不了,必须由管理员显式授予。官方文档按安全性从高到低列了五种授予姿势:

  1. 授予给应用专属的 ServiceAccount(最佳实践)。Pod spec 里显式指定 serviceAccountName,再为这个 SA 建绑定。
  2. 授予命名空间的 default SA。注意:所有没写 serviceAccountName 的 Pod 都会用它,等于权限对命名空间里所有应用开放。
  3. 授予命名空间内所有 SA(组 system:serviceaccounts:<ns>)。
  4. 集群级授予所有 SA(组 system:serviceaccounts),不推荐。
  5. 给所有 SA 集群管理员权限,强烈不推荐——官方 warning 指出,任何能读 Secrets 或能创建 Pod 的用户因此获得整个集群的完全访问权。
# 姿势一:只给 my-sa 在 my-namespace 内的只读权限
kubectl create rolebinding my-sa-view \
  --clusterrole=view \
  --serviceaccount=my-namespace:my-sa \
  --namespace=my-namespace

主体的写法上有几个极易混淆的字符串,官方用 note 专门强调:SA 用户名的前缀是 system:serviceaccount:(单数),SA 组的前缀是 system:serviceaccounts:(复数),再往后拼命名空间和名字,如 system:serviceaccount:kube-system:coredns。system:authenticated 代表所有已认证用户,system:unauthenticated 代表未认证用户,system: 前缀整体保留给系统用,自定义用户和组不要占用。

关于令牌本身,ServiceAccount token 的投射(projection)从 v1.20 起稳定。kubelet 可以把一个指定 audience 和有效期(TTL)的 JWT 投射进 Pod,用 projected volume 的 serviceAccountToken 源配置。它需要 API 服务器配置 --service-account-issuer、--service-account-key-file、--service-account-signing-key-file(可选 --api-audiences)。投射令牌的 payload 是结构化的:aud、exp、iat、iss、jti,以及 kubernetes.io 下的命名空间、节点名与 UID、Pod 名与 UID、SA 名与 UID。因为绑定了 Pod 和 SA,Pod 或 SA 被删除时令牌立即失效。kubelet 会在令牌寿命超过 80% 或超过 24 小时时主动轮换,应用负责在轮换后重新加载(按固定周期读文件即可,不必精确跟踪过期时间)。

apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  serviceAccountName: build-robot
  containers:
  - image: nginx
    name: nginx
    volumeMounts:
    - mountPath: /var/run/secrets/tokens
      name: vault-token
  volumes:
  - name: vault-token
    projected:
      sources:
      - serviceAccountToken:
          path: vault-token
          expirationSeconds: 7200
          audience: vault

这个机制的典型用途是向 Vault 等外部系统证明身份:audience 设为外部系统的标识,外部系统验证 JWT 的签发者(issuer discovery 自 v1.21 起稳定,要求 issuer URL 符合 OIDC Discovery 规范,即 https 协议并在 {issuer}/.well-known/openid-configuration 提供配置)后,把 K8s 身份映射为自己的策略。与拿 SA token 直连 API 服务器相比,audience 绑定让令牌即使泄露也不能拿去访问 API,攻击面小得多。

官方 ServiceAccount 任务文档还提到:可以为 SA 关闭 API 凭据自动挂载(automountServiceAccountToken: false),对不需要访问 API 的工作负载应当关掉——进程被攻破时,挂载在 /var/run/secrets/kubernetes.io/serviceaccount 的 token 就是攻击者的免费门票。

你现在应该掌握了什么:SA 默认零权限,按官方五种姿势从「专属 SA」开始逐级放宽;单数/复数前缀分别是用户名和组;投射令牌带 audience 和 TTL、绑定 Pod 与 SA、由 kubelet 自动轮换,是对接外部身份系统的推荐方式;不需要 API 访问的工作负载关掉凭据自动挂载。

聚合 ClusterRole 与权限边界

ClusterRole 可以带 aggregationRule,控制平面里的控制器会监视这类对象,把匹配标签选择器的其他 ClusterRole 的规则合并进它的 rules 字段。这带来一种可扩展的权限组装模式:平台方维护一个聚合角色(比如 monitoring),各团队通过创建带特定标签的 ClusterRole 往里面「加料」,而不需要平台方修改中心角色。

官方 caution 在这里非常具体:控制平面会覆盖聚合 ClusterRole 里手工写的 rules;用 server-side apply 应用清单时,即使把 rules 写成空列表也会声明字段所有权,导致与应用方(GitOps 控制器常见)之间反复冲突——要么 apply 报字段管理冲突失败,要么强制覆盖后控制平面又填回来。正确做法是聚合角色的清单里干脆不写 rules 字段。

默认的面向用户角色(admin/edit/view)本身就是聚合角色,标签是 rbac.authorization.k8s.io/aggregate-to-admin: "true"、aggregate-to-edit、aggregate-to-view。这意味着你要让默认角色「认识」自己的 CRD,比如一个名为 CronTab 的自定义资源(URL 里的资源名是 crontabs),只需要创建两个带对应标签的 ClusterRole:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: aggregate-cron-tabs-edit
  labels:
    rbac.authorization.k8s.io/aggregate-to-admin: "true"
    rbac.authorization.k8s.io/aggregate-to-edit: "true"
rules:
- apiGroups: ["stable.example.com"]
  resources: ["crontabs"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

那么「权限边界」如何理解?聚合机制把「定义权限」和「组合权限」解耦了,但边界仍然由绑定决定:聚合只改变角色里装了哪些规则,不改变规则在哪些命名空间对谁生效。再加上 escalate/bind 限制,一条权限从「被某个 ClusterRole 收录」到「真正授予某个主体」,中间隔着绑定和提权校验两道闸。设计扩展 APIServer(aggregated API)时,官方默认角色 system:auth-delegator(允许委托认证和授权检查)就是给这类场景准备的——扩展 API 服务器可以用它把认证授权委托给主 API 服务器统一裁决,配合 SubjectAccessReview 实现权限穿透校验。

你现在应该掌握了什么:aggregationRule 用标签选择器把多个 ClusterRole 的规则合成一个,控制平面拥有 rules 字段所以聚合角色清单不要写 rules;默认 admin/edit/view 角色靠三个 aggregate-to-* 标签扩展;聚合只组装规则,生效范围仍由绑定和 escalate/bind 校验决定。

越权排查与权限审计命令

权限问题的排查,官方给出的核心工具是 kubectl auth can-i。它底层走 SelfSubjectAccessReview API,与集群实际使用的授权模式无关——只要 API 服务器能裁决,答案就是权威的。

# 我能不能在 dev 命名空间创建 deployments?
kubectl auth can-i create deployments --namespace dev
# dave 能不能在 dev 命名空间 list secrets?(用户模拟)
kubectl auth can-i list secrets --namespace dev --as dave
# 输出: no

# dev 命名空间的 dev-sa 能不能在 target 命名空间 list pods?
kubectl auth can-i list pods \
    --namespace target \
    --as system:serviceaccount:dev:dev-sa

--as 属于用户模拟(impersonation)能力,本身需要 impersonate 动词授权,所以普通开发者通常只能模拟自己,管理员才能替别人「试穿」权限。除 can-i 外,authorization.k8s.io 组还有三个审查 API:SubjectAccessReview(查任意主体,扩展 API 服务器和 kubelet 就用它把授权决策委托给 API 服务器)、LocalSubjectAccessReview(限定命名空间版)、SelfSubjectRulesReview(列出自己在某命名空间能做的全部动作,UI 用它隐藏不可用的按钮)。

把 can-i 串成脚本,就可以做「权限漂移审计」:对关键角色定期执行 can-i 检查,比对预期清单:

#!/usr/bin/env bash
# 审计:某 SA 是否拥有超出预期的权限
SA="system:serviceaccount:prod:myapp"
for verb in get list watch create update patch delete; do
  for res in secrets pods deployments; do
    if kubectl auth can-i "$verb" "$res" -n prod --as "$SA" 2>/dev/null | grep -q yes; then
      echo "ALLOWED: $verb $res (prod) for $SA"
    fi
  done
done

API 服务器侧,把 RBAC 组件的日志级别调到 5(--v=5 或 --vmodule=rbac*=5),deny 记录会以 RBAC 前缀出现在 apiserver 日志里。官方在「从 ABAC 升级」一节正是用这个手段做迁移:RBAC 和 ABAC 并行运行(--authorization-mode=...,RBAC,ABAC --authorization-policy-file=mypolicy.json),只要任一授权器放行就允许;然后观察日志里的 RBAC 拒绝记录,逐个补齐角色,直到日志干净再移除 ABAC。这个「双跑观察法」对任何权限体系迁移都是教科书级的操作流程。

另一个常用排查思路是直接看对象:kubectl describe rolebinding -n <ns>、kubectl get clusterrole <name> -o yaml(如 kubectl get clusterroles system:discovery -o yaml)。记住自动协调机制:如果你改了默认角色,重启 apiserver 后被改回去,别怀疑自己记错。

你现在应该掌握了什么:can-i + --as 是权限排查的第一工具;三个 AccessReview API 支撑程序化授权检查;--v=5 的 RBAC 日志是迁移期的照妖镜;排查时永远记得默认角色会被自动协调恢复。

多租户隔离实践

把前面所有机制串起来,就是一套多租户方案。官方文档没有单独给「多租户」一章,但各处的机制组合起来指向同一套实践。以「团队 A、团队 B 共享一个集群」为例:

  • 命名空间是隔离边界。每个团队一个命名空间,资源配额(ResourceQuota)、限制范围(LimitRange)、网络策略等配额类对象都挂在命名空间上。注意内置 admin 角色明确不允许写资源配额,也不允许写命名空间本身——这正好符合「平台管配额、团队管负载」的分工。
  • 团队内用 RoleBinding + ClusterRole(view/edit)授予日常权限,而不是把 edit 做成 ClusterRoleBinding。一旦 ClusterRoleBinding 化,某个团队就能读全集群的 Secrets 之外的几乎所有对象,隔离名存实亡。
  • CI/CD 系统给每个项目发专属 SA + 最小角色,配合 resourceNames 把发布机器人钉在它要更新的那几个 Deployment/ConfigMap 上。授予动作本身用前文「授权员」模式圈定范围,防止流水线账号自我提权。
  • 敏感操作走 escalate/bind 显式审批:普通开发者不能创建含 secrets 权限的角色,平台管理员用 escalate 动词白名单控制哪些权限可以被「下放」。
  • 控制面组件与节点用各自默认的 system: 角色,不要为了「省事」把 system:node 发给真实节点——官方要求用 Node 授权器加 NodeRestriction,把 kubelet 的权限收敛到「只管自己节点上的 Pod」。
  • EndpointSlices 写权限是个历史坑:v1.22 之前创建的集群,edit/admin 聚合角色里包含 EndpointSlices 写权限;作为 CVE-2021-25740 的缓解,v1.22 起新建集群不再包含,但升级上来的老集群保持原样,需要按 CVE 公告手工收敛。多租户场景里这意味着:老集群上拿到 edit 角色的租户可能篡改 Service 的后端端点,把流量劫走——务必排查。
  • 面向未来的集群,开启审计日志(官方 RBAC 文档之外的能力,具体配置以当前版本官方文档为准),把授权决策留痕,配合 can-i 定期做权限复核。

把租户视角的权限模型总结成一张对照表:

租户诉求 推荐做法 不推荐做法 风险点
日常开发读写本命名空间负载 RoleBinding 绑 edit/view ClusterRoleBinding 绑 edit 跨命名空间数据暴露
发布机器人更新特定配置 Role + resourceNames 钉死 复用 default SA 或直接给 admin 机器人凭证泄露即提权
团队自助授权 授权员角色 + bind 白名单 给 cluster-admin 无法收敛的权限蔓延
应用读 Vault 等外部系统 Token 投射 + audience 绑定 把 SA token 拷贝出集群使用 令牌无过期无绑定
节点接入 Node 授权器 + NodeRestriction 发放 system:node 任一节点可读全集群 Secrets

你现在应该掌握了什么:命名空间 + RoleBinding 是租户隔离的地基;配额类对象由平台持有,admin 角色天然写不了;发布账号最小化到 resourceNames;自助授权用 bind 白名单收口;老集群的 EndpointSlices 写权限是 CVE-2021-25740 遗留的必查项。

踩坑经验

以下四组错误来自官方文档明确警告的场景和最常见的生产事故模式。

第一组:给所有 ServiceAccount 发集群管理员

❌ 错误做法:

kubectl create clusterrolebinding permissive-binding \
  --clusterrole=cluster-admin \
  --group=system:serviceaccounts

官方文档对这条命令给出的原文警告是:它允许所有 ServiceAccount 成为集群管理员,任何运行在容器里的应用自动拿到 SA 凭据,可以对 API 执行任何动作,包括查看 secrets 和修改权限,这不是一条推荐策略。

✅ 正确做法:为每个应用创建专属 SA,用 RoleBinding 在单个命名空间授予最小权限:

kubectl create serviceaccount myapp-sa -n my-namespace
kubectl create rolebinding myapp-sa-view \
  --clusterrole=view \
  --serviceaccount=my-namespace:myapp-sa \
  --namespace=my-namespace

第二组:以为「只给了 list 没给 get」就安全

❌ 错误做法:给监控账号 get secrets 之外的所有权限,认为 secrets 因此不可见。

✅ 正确做法:官方 caution 指出 get、list、watch 在数据暴露上等价,对 secrets 的 list 会返回所有对象的 data 字段。对敏感资源要么不授予任何读动词,要么用 resourceNames 精确到单个对象并接受其限制(list 需带 metadata.name 字段选择器)。

第三组:用通配符写「临时」角色

❌ 错误做法:

rules:
- apiGroups: ["example.com"]
  resources: ["*"]
  verbs: ["*"]

✅ 正确做法:官方明确警告通配符会对未来新增的资源类型、子资源和自定义动词自动放行,属于过度授权。最小权限原则要求显式枚举资源与动词,例如 resources: ["crontabs"]、verbs: ["get", "list", "watch"]。

第四组:改绑定换角色,或者改默认角色

❌ 错误做法:kubectl edit rolebinding 修改 roleRef;或直接 kubectl edit clusterrole admin 想给默认角色加权限。

✅ 正确做法:roleRef 不可变是 API 层强制的,换角色必须删除重建(批量场景用 kubectl auth reconcile);默认角色每次 apiserver 启动会被自动协调恢复,想扩展 admin/edit/view 应创建带 rbac.authorization.k8s.io/aggregate-to-admin: "true" 等标签的聚合 ClusterRole。

第五组:聚合 ClusterRole 的清单里写了 rules

❌ 错误做法:在带 aggregationRule 的 ClusterRole 里手写 rules 字段,甚至写成空列表,再用 GitOps 做 server-side apply。

✅ 正确做法:聚合角色的清单里省略 rules 字段。控制平面拥有并填充该字段,声明所有权只会导致 apply 冲突或反复清空再回填的拉锯战。

第六组:客户端证书当长期账号用

❌ 错误做法:给每个员工签一张三年有效期的客户端证书,离职时靠回收 kubeconfig 文件。

✅ 正确做法:官方明确 Kubernetes 不支持证书吊销,证书在过期前一直有效。人员身份应走 OIDC 等可即时失效的外部集成,客户端证书只用于受控的组件间通信并配合短有效期。

最佳实践清单

把全文的结论浓缩成可执行清单:

  1. 启用 RBAC 走 --authorization-config 文件方式,authorizers 链中显式排列 Node、RBAC,需要时再接 Webhook;不要与 --authorization-mode 混用,启动会直接报错。
  2. 权限永远显式枚举:apiGroups、resources、verbs 不用 *;能收窄到对象就加 resourceNames,能收窄到命名空间就用 RoleBinding 而不是 ClusterRoleBinding。
  3. 每个工作负载一个专属 ServiceAccount,Pod 显式写 serviceAccountName;不需要 API 访问的负载设置 automountServiceAccountToken: false。
  4. 对外身份集成用 Token 投射 + audience 绑定,kubelet 会自动轮换,应用定时重读文件即可。
  5. 自助授权平台用 escalate/bind 白名单模式,把可授予的角色集合用 resourceNames 钉死,绝不发放裸的 cluster-admin。
  6. 不改 system: 前缀默认角色;要扩展 admin/edit/view 用聚合标签;绑定变更需要换角色时用 kubectl auth reconcile 管理。
  7. 升级过 v1.22 的老集群,排查 edit/admin 里的 EndpointSlices 写权限(CVE-2021-25740 遗留)。
  8. 排查三件套:kubectl auth can-i(必要时配 --as)、--v=5 的 RBAC 日志、定期用 can-i 脚本做权限漂移审计。

总结

  1. Kubernetes 访问控制分认证、授权、准入三层:认证把凭证翻译成用户与组,授权默认拒绝、多授权器按序短路裁决,RBAC 只是授权器之一但最常用。
  2. RBAC 是纯累加模型,四种对象各司其职:Role/ClusterRole 装规则,RoleBinding/ClusterRoleBinding 绑主体;RoleBinding 引用 ClusterRole 时权限仍被限制在绑定所在命名空间,这是权限收敛的关键手法。
  3. 最小权限要落到动词粒度:HTTP 方法到 create/get/list/watch/update/patch/delete/deletecollection 的映射表是设计角色的字典;list 与 get 在数据暴露上等价,敏感资源的读权限必须整体考量。
  4. ServiceAccount 默认零权限,官方五种授予姿势从专属 SA 到全员 cluster-admin 安全性递减;Token 投射让工作负载以带 audience、TTL 且绑定 Pod 的 JWT 对接外部系统,是身份联邦的正确姿势。
  5. escalate/bind 两道校验把「定义权限」和「授予权限」分开管控,配合聚合 ClusterRole 实现了可扩展又不失边界的权限体系。
  6. 排查靠 kubectl auth can-i --as、RBAC 详细日志和 AccessReview API;多租户隔离靠命名空间 + RoleBinding + 配额分工,老集群务必清理 EndpointSlices 写权限遗留。

参考资料