LabHub
学习 学习路径 课程

KCSA — Kubernetes 安全助理

一个请求穿过 apiserver 的路

在 LabHub 中继续学习

一句话总结

进入 apiserver 的请求会依次经过认证 → 授权 → 准入 → 验证 → 存储。只要理解每个阶段判断什么、不判断什么,大多数安全问题就能迎刃而解。

一个请求通过 apiserver 的六个阶段——认证、授权、Mutating 准入、模式验证、Validating 准入、写入 etcd。401 是第 1 阶段失败,403 是第 2 阶段失败;准入必须先修改、后检查才能保持逻辑一致,因此 Mutating 在前

流程图: 认证 → 授权 → 准入 → 验证 → 存储 · 依次 · 一个重要事实:Kubernetes 中没有 User 对象。 · --anonymous-auth=true

为什么需要它

要回答“这个请求为什么被拒绝”,必须知道它卡在哪个阶段。401 是认证失败,403 是授权失败,准入 Webhook 的拒绝又会显示另一种消息。不理解阶段,就会在应该修复 RBAC 时去改证书。

工作原理

第 1 阶段:认证(Authentication)——“你是谁”

apiserver 会依次尝试多个认证器(authenticator),只要一个成功,就以该身份继续。全部失败时,请求者会成为 system:anonymous(所属组为 system:unauthenticated)。

方式 使用场景
X.509 客户端证书 kubelet、控制器、管理员 kubeconfig;CN 是用户名,O 是组
ServiceAccount 令牌(JWT) 从 Pod 内调用 API
OIDC 令牌 把人类用户与公司 IdP 连接
Webhook 令牌认证 委托给外部认证系统

一个重要事实:Kubernetes 中没有 User 对象。 用户只是认证器生成的字符串,因此无法“删除用户”。必须吊销证书,或从 IdP 中移除。

--anonymous-auth=true 不会直接拒绝认证失败,而是赋予匿名身份。匿名用户可以做什么由授权阶段决定,因此,真正的问题不是匿名访问本身,而是赋予匿名身份的权限。默认集群中,匿名用户只能查看 /healthz/version 等公开信息。

system:masters 组非常特殊。 该组成员绕过所有 RBAC 检查,即使删除 RoleBinding 或 ClusterRoleBinding 也无法撤销权限。如果证书中写有 O=system:masters,在证书有效期间,它实际上就是无法撤销的最高权限。

第 2 阶段:授权(Authorization)——“可以做这件事吗”

授权模块也按顺序评估,只要其中一个允许,请求就通过。使用 --authorization-mode 选项指定。

RBAC 只有允许,没有拒绝。没有任何规则允许时,就是拒绝。因此,无法创建“从某人那里夺走权限的 Role”,只能删除相应绑定。

第 3 阶段:准入(Admission)——“内容符合策略吗”

即使通过授权,还要检查请求内容是否符合规则。顺序很重要:先 Mutating,后 Validating。 必须先修改,再检查,逻辑才一致。

这一阶段的典型内置插件是 PodSecurity(PSA)。外部策略引擎(OPA Gatekeeper、Kyverno)也以 Webhook 形式接入这一阶段。

准入 Webhook 存在运维陷阱。failurePolicyFail 时,Webhook 宕机会导致所有 Pod 创建都被拒绝;设为 Ignore 时,整个策略会被绕过。作者博客给出的判断很明确:“生产环境保持 Fail,同时为 Gatekeeper Pod 提供充足的资源和 replicas。” 也就是说,不要削弱策略来解决可用性问题,而应通过工程手段解决。

第 4 阶段:写入 etcd 与静态存储加密

apiserver 把对象序列化(默认 protobuf)后,经过 transformer 写入 etcd。transformer 为 identity 时写入明文;为 aescbc/aesgcm/kms 时写入密文。

apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources: [secrets]
    providers:
      - aescbc:
          keys: [{name: key1, secret: <base64>}]
      - identity: {}

providers 数组的顺序决定一切。 第一个 provider 用于写入,所有 provider 都用于读取。因此,把 identity 放在最前面会恢复为明文存储;放在最后则表示“新写入内容加密,但旧明文仍可读取”。而且,启用该配置后,现有 Secret 在重新写入前仍保持明文,需要执行类似 kubectl get secrets -A -o json | kubectl replace -f - 的全量重写。

KMS v2 provider 更进一步:DEK 在本地生成并缓存,远程 KMS 只管理 KEK(信封加密)。轮换密钥不需要重启 apiserver。

第 5 阶段:kubelet 的认证与授权

kubelet 在两个方向上拥有身份。

controller-manager 与令牌签发

controller-manager 运行 ServiceAccount 控制器与令牌控制器,并使用 --service-account-private-key-file 提供的密钥为 SA 令牌签名。取得该密钥,就能自行签发冒充任意 SA 的令牌。 因为 apiserver 只用对应公钥验证。

从 v1.24 起,创建 SA 后不再自动生成永久令牌 Secret;默认使用基于 TokenRequest API 的限时令牌kubectl create token <sa> --duration=3600s),这是重要的安全改进。由此也能理解为何 --service-account-lookup=false 很危险:设为 false 后,已经删除的 SA 的令牌,只要签名仍有效,就会继续通过。

实际现场中的情况

作者扩展家庭实验室的记录,直接展现了这套体系的关键点。增加控制平面时需要 CA 私钥,因为新节点也必须成为签发证书的主体。kubeadm 不要求通过 scp 传输该文件,而是使用 kubeadm init phase upload-certs --upload-certs,把它加密后作为集群内 Secret 上传,并提供一个 64 位十六进制 certificate-key 作为密文钥匙。

而且,这个 Secret 会在 2 小时后自动删除。 这是为了尽量缩短集群根信任 CA 私钥即使以加密形式存在于集群内的时间。它很好地展示了“缩短敏感资料暴露窗口(window)”原则如何实现为产品功能。

还有一点:该家庭实验室没有把控制平面扩展到 2 台就停下,而是直接扩展到 3 台。原因很清楚:etcd quorum 要求过半,而 2 个成员的过半仍是 2,因此 2 个成员的故障概率反而比 1 个成员更高。 两者中任何一个故障都会导致无法写入。这是可用性设计中“越多越好”并不成立的典型案例。

下一项实验要做什么

下一项实验将亲自列出 apiserver 的危险选项,按正确 provider 顺序编写 EncryptionConfiguration YAML,并整理 kubelet 检查项。然后在两个命名空间中配置服务账户与 Role,通过 kubectl auth can-i 证明隔离确实生效,并观察和记录匿名用户当前可以在该集群中执行哪些操作。