令牌、角色、准入,以及会消失的 API 版本
一句话总结
Pod 发往 API 服务器的请求会依次经过认证 → 授权 → 准入三道关卡。服务账号令牌会被检查签名、过期时间、绑定对象和受众(audience);RBAC 用只增不减的规则划定允许范围;准入控制器会修改 spec(mutating)或直接拒绝(validating)。此外,API 版本会按既定规则下线,所以要用 kubectl api-resources 查看当前服务器实际提供哪些 API,再用 kubectl convert 迁移清单。
为什么需要它
开发者遇到的症状总是那三种:“401 Unauthorized”、“forbidden”,以及“我写的 YAML 和实际运行的 Pod 不一样”。第一种是认证的结果,第二种是授权的结果,第三种是准入的结果。如果分不清这三者,就会出现放开了 RBAC 却仍然返回 401,或者换了令牌却依旧 forbidden 的情况。第四种症状出现在升级之后——昨天还能用的 kubectl apply 现在以“no matches for kind”失败。原因是服务器不再提供该 API 版本,这不是事故,而是 API 弃用策略 早已预告的时间表。
工作原理
认证——服务账号令牌如何被检查
根据服务账号文档,服务账号使用已签名的 JWT 向 API 服务器认证。客户端附带 Authorization: Bearer <token> 请求头,API 服务器按以下顺序检查:令牌签名、是否过期、令牌声明(claim)所指向的对象引用现在是否仍然有效、令牌此刻是否处于有效时间内,最后是 audience 声明。通过 TokenRequest API 签发的令牌会绑定到 Pod 这类客户端的生命周期,因此 API 服务器还会确认该对象是否仍以原来的唯一 ID 存在。保存在 Secret 中的旧式令牌则与该 Secret 比对。
签名密钥由服务账号管理文档说明。kube-controller-manager 中的令牌控制器用 --service-account-private-key-file 指定的私钥为令牌签名,kube-apiserver 则用 --service-account-key-file 指定的公钥验证。令牌进入 Pod 的途径,是 ServiceAccount 准入控制器附加的 projected 卷(自 1.22 起稳定,无法关闭)。
- name: kube-api-access-<random-suffix>
projected:
sources:
- serviceAccountToken:
path: token
- configMap:
name: kube-root-ca.crt
items: [{key: ca.crt, path: ca.crt}]
- downwardAPI:
items: [{fieldRef: {fieldPath: metadata.namespace}, path: namespace}]
三个来源中,第一个是关键。kubelet 通过 TokenRequest API 获取有时间限制的令牌并放入 Pod(默认有效期 1 小时),在过期前续期;该令牌绑定到这个 Pod,并以 kube-apiserver 为 audience。旧方式是永不过期的、基于 Secret 的令牌,这套机制取代了它。不需要令牌的 Pod,按照服务账号配置文档,在 ServiceAccount 或 Pod spec 中设置 automountServiceAccountToken: false 来关闭挂载(两处都有设置时,以 Pod spec 为准)。面向外部系统时,可以像同一文档的示例那样使用指定了 audience: vault 和 expirationSeconds: 7200 的 projected 令牌;kubelet 会在 TTL 过去 80% 或超过 24 小时后请求更换,所以应用需要定期重新读取该文件。
授权——RBAC 只做加法
RBAC 文档的核心一句是:权限是纯粹累加的,不存在拒绝规则。Role 是命名空间内的权限;ClusterRole 不属于任何命名空间,用来定义对集群范围资源的权限,或可在多个命名空间中复用的权限集合。RoleBinding 把 Role 或 ClusterRole 在某个特定命名空间内授予主体(用户、组、服务账号),ClusterRoleBinding 则作用于整个集群。
RBAC 良好实践所说的最小权限很具体:尽可能在命名空间级别授权;用 RoleBinding 代替 ClusterRoleBinding,把范围限定在特定命名空间内;避免通配符(这等于把权限授予现有的以及将来出现的所有资源类型);只在确有必要时才使用 cluster-admin。对开发者来说,这可以落实为:“如果我的 Pod 的服务账号只需要读取 ConfigMap,就在该命名空间创建一个只有 get 和 list 的 Role 和 RoleBinding。”
准入——spec 在这里被修改或拒绝
根据准入控制器文档,准入控制器是 kube-apiserver 内部的代码,会检查创建、删除、修改对象的请求中的数据。读取请求(get、list、watch)不经过准入。它分两个阶段运行:先由 mutating 控制器修改数据,再由 validating 控制器检查,任何一个阶段只要有一个控制器拒绝,整个请求就会被拒绝。1.37 的默认启用列表包括 CertificateApproval、DefaultIngressClass、DefaultStorageClass、DefaultTolerationSeconds、LimitRanger、MutatingAdmissionPolicy、MutatingAdmissionWebhook、NamespaceLifecycle、PersistentVolumeClaimResize、PodSecurity、Priority、ResourceQuota、RuntimeClass、ServiceAccount、StorageObjectInUseProtection、TaintNodesByCondition、ValidatingAdmissionPolicy、ValidatingAdmissionWebhook 等。
只挑开发者每天都会碰到的,大致如下。
| 控制器 | 类型 | 对我的 spec 做什么 |
|---|---|---|
| ServiceAccount | mutating + validating | 附加服务账号令牌卷 |
| LimitRanger | mutating + validating | 违反命名空间的 LimitRange 时拒绝;未填写请求量时填入默认值 |
| DefaultStorageClass | mutating | 给没有 storageClassName 的 PVC 填入默认 StorageClass |
| NamespaceLifecycle | validating | 在正在终止或不存在的命名空间中创建对象时拒绝 |
| PodSecurity | validating | 拒绝不符合命名空间 Pod Security 标签的 Pod |
| ResourceQuota | validating | 超出命名空间配额时拒绝 |
它还有四个扩展点。MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook 会调用通过 API 注册的外部 Webhook;ValidatingAdmissionPolicy 无需调用外部服务,而是用 CEL(Common Expression Language)在 API 内部声明验证规则;MutatingAdmissionPolicy 是同样方式的修改。所以,如果用 kubectl get pod -o yaml 看到的 Pod 与自己提交的 YAML 不同——多了令牌卷、请求量被填充,或是被附加了 sidecar——那不是有人改了它,而是 mutating 阶段做的事。至于是哪个准入拒绝了请求,错误消息中会出现控制器的名称。
API 弃用——按既定规则下线
API 弃用策略指出,每个 API 组独立带版本,版本遵循 alpha(v1alpha1)、beta(v1beta1)、GA(v1)三条轨道。规则 1:API 元素只能通过提升 API 组版本的方式移除,一旦进入某个版本的元素,不会从该版本中去掉,也不会发生大的改变。规则 2:在同一个发布版本内,对象必须能够在各版本之间往返转换而不丢失信息。规则 3:不能为了不够稳定的版本而被弃用(GA 可以取代 beta,但 beta 不能取代 GA)。规则 4a:生命周期由稳定性级别决定——GA 版本可以被标记为弃用,但在 Kubernetes 主版本内不会被移除;beta 版本在引入后 9 个月或 3 个 minor 版本(取较长者)内被弃用,弃用后再过 9 个月或 3 个 minor 版本(取较长者)停止提供服务;alpha 版本可以在任意发布版本中不经预告就被移除。
已弃用 API 迁移指南按发布版本列出了停止提供服务的版本。例如 v1.32 不再提供 flowcontrol.apiserver.k8s.io/v1beta3 的 FlowSchema 和 PriorityLevelConfiguration,需要迁移到自 v1.29 起就有的 v1。迁移步骤同样写在这份文档里。
- 查找。在 1.19 及以上版本中,可以通过客户端警告、指标和审计信息找到使用已弃用 API 的位置。当前服务器提供哪些 API,用 kubectl api-resources 查看:用
-o wide可以连同支持的 verb 一起显示,用--api-group=<그룹>(占位符为 API 组名)只看特定组,用--namespaced=false只看集群范围的资源。 - 试验。给 API 服务器加上
--runtime-config=<그룹>/<버전>=false(占位符依次为 API 组和版本),提前关闭即将移除的版本,确认哪些东西会出问题。 - 迁移。把控制器和集成代码改为调用未被弃用的 API,YAML 则用
kubectl convert -f <파일> --output-version <그룹>/<버전>(占位符依次为文件名、API 组和版本)转换。例如旧的 Deployment 用--output-version apps/v1。转换可能填入并不理想的默认值,所以要把结果与 API 参考对照。kubectl convert曾经内置在 kubectl 中,现在是默认安装里没有的插件,需要按安装文档的步骤另行下载。
在现场相遇的样子
放开了 RBAC,却一直返回 401。 401 是认证失败。授权(RBAC)是通过认证之后的关卡,所以无论把 Role 放得多宽,401 都不会改变。典型情形是:Pod 挂载的令牌已经过期,而应用只在启动时读取一次、之后不再重新读取——这正是文档要求定期重新读取的原因。
PVC 没写 storageClassName,却挂上了某个 StorageClass。 这是 DefaultStorageClass 准入填入的。如果没有默认 StorageClass,它什么也不做;如果有两个以上被标记为默认,则遵循文档另行规定的规则。“里面有我没写的值”,大多是 mutating 准入留下的痕迹。
下一项测验要确认什么
测验会考查服务账号令牌的检查项目和默认有效期、签名密钥所在的位置、RBAC 的累加性质和绑定范围、准入的两个阶段与读取请求、beta API 的生命周期规则,以及 kubectl convert 的用途。