TT Lab
开始
学习 学习路径 课程

策略即代码

Pod 安全标准与准入 — 等级和模式是不同的维度

在 TT Lab 中继续学习

一句话总结

Pod Security Standards 规定的是禁止什么(privileged、baseline、restricted),Pod Security Admission 规定的是如何处理违规(enforce、audit、warn),这两者是不同的维度,所以可以在命名空间标签中自由组合。

为什么需要它

Pod 规格中有好几个能把宿主机整个打开的开关:hostNetwork、hostPID、privileged: true、hostPath 卷、任意添加的 capability。只要打开其中一个,容器隔离实际上就消失了。可是这些字段,又确实是正常的基础设施工作负载(CNI 代理、日志收集器、节点导出器)实际在使用的。这是一个不是“禁止”,而是必须确定“允许到什么程度”的问题,所以单纯的禁止列表解决不了。

PodSecurityPolicy 曾经承担过这个位置,但在 v1.25 中被移除了。PSP 看的不是用户,而是创建 Pod 的主体(通常是控制器的服务账号)的权限,而且当 Pod 上有多个 PSP 时,很难预测会适用哪一个。取而代之的是 Pod Security Admission。规则由项目确定并固定为三个等级,适用单位则简化为一个命名空间标签。

工作原理

维度一——等级(Pod Security Standards)。

等级 性质
privileged 没有限制。用于系统和基础设施工作负载
baseline 阻止已知的权限提升的最小限制。默认的 Pod 规格可以原样通过
restricted 强烈要求遵循 Pod 加固的最佳实践

baseline 所阻止的,是共享宿主机命名空间、privileged、允许列表之外的 capability、hostPath 卷、宿主机端口,以及绕过 AppArmor、SELinux、seccomp 之类的做法。restricted 在此基础上又增加了限制卷类型、allowPrivilegeEscalation: false、runAsNonRoot: true、runAsUser 不设为 0、显式指定 seccomp 配置文件、drop 所有 capability 而只允许 NET_BIND_SERVICE。这里有一个重要的差别。baseline 只要“不使用奇怪的东西”就能通过,而 restricted 连普通的清单也通不过。因为什么都没写的容器,没有 seccomp 配置文件,也没有 drop capability。

维度二——模式(Pod Security Admission)。

模式 违规时
enforce 拒绝 Pod
audit 在审计日志的事件中添加注解,并放行
warn 向用户显示警告,并放行

两个维度通过标签相遇。每个模式有两个标签。

apiVersion: v1
kind: Namespace
metadata:
  name: my-baseline-namespace
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: v1.30
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

这个组合本身就是引入流程。现在只强制 baseline,restricted 的违规用警告和审计来统计。一个命名空间可以设置全部三个模式,而且每个模式可以指定不同的等级,所以“提前打开下一个目标并观察”,只需两行标签就能做到。

漏掉 -version 后缀,升级那天部署就会停摆。版本标签的值是有效的 Kubernetes 次版本号或 latest,不写的话按 latest 运行。各个等级的内容会随版本变化——实际上 restricted 在 v1.25 中就发生了变化。如果设成 latest,升级集群的那天,明明什么清单都没改,昨天还能用的部署却被拦住了。反过来,如果固定了版本,即使等级变强,当天也什么都不会发生,而升级版本的时间表由人来定。这是把策略变更与集群升级分离开的机制。

准入是查看请求的机制。它无法拦住已经在运行的 Pod。贴上标签之后,违规的 Pod 仍然继续运行,只有当它重启或被重新调度时才会被拒绝。所以在切换之前,需要有提前找出违规的流程。用服务端 dry-run 运行标签命令,该命名空间中已有的 Pod 的违规会以警告形式出现。

kubectl label --dry-run=server --overwrite ns payments \
  pod-security.kubernetes.io/enforce=restricted

文档中还写有一个容易忽略的事实。enforce 不适用于工作负载资源,只适用于由此产生的 Pod。audit 和 warn 也适用于 Deployment 这样的工作负载。所以带有违规模板的 Deployment,kubectl apply 会成功(会出现警告),而 Pod 没有生成这件事,要等几秒后在 ReplicaSet 的事件中才会暴露出来。这是“部署成功了,可是没有 Pod”这类咨询的常见原因。

豁免(exemption)不是写在标签上,而是写在准入控制器的配置文件中,共有三个维度——用户名、RuntimeClass 名称、命名空间。文档警告说,不要豁免控制器的服务账号。如果豁免了 replicaset-controller,所有能创建工作负载的人都会被隐式豁免。

在现场相遇的样子

第一,直接把 restricted 启用为 enforce 的团队。普通的清单全都被拦住。对于从来没写过 securityContext 的团队来说,这意味着“要把所有 Deployment 都改一遍”,所以标签通常一天之内就被撤下来。正确的顺序是:用 warn、audit 启用 restricted,而 enforce 停留在 baseline。

第二,在系统命名空间上设置了 restricted。CNI 代理或节点导出器一重启,就起不来了。集群陷入无法自行恢复的状态,而要知道原因是标签,得打开 Pod 事件才行。

第三,集群升级那天的故障。只有没有写 -version 就启用的命名空间,部署被拦住了。清单和策略都没变,所以很久才找到原因。固定版本不是偷懒,而是变更管理。

第四,用指标来看的方法。kube-apiserver 会输出 pod_security_evaluations_total、pod_security_errors_total、pod_security_exemptions_total。在以 audit 模式运行的期间,把这些值放到仪表板上,就能用数字回答“可以提升到 enforce 吗”。

参考文档

下一项实验要做什么

在 kwok 集群的命名空间上亲手贴标签,走完一遍切换流程。先启动违规 Pod,用 --dry-run=server 试用标签,确认已有的违规会以警告形式出现,在只开启 warn 和 audit 的状态下,看到同一个 Pod 能通过,然后提升到 enforce,确认它被拒绝。把固定了 -version 的命名空间和使用 latest 的命名空间并排放置,并亲手制造带有违规模板的 Deployment,apply 成功而 Pod 却没有生成的场景。