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

策略即代码

只开着警告就切到 enforce,结果部署全停了

在 TT Lab 中继续学习

目标

只用命名空间标签来运用 Kubernetes 内置的 Pod 安全准入(PSA)。亲自走一遍从观测(warn、audit)提升到拦截(enforce)的顺序,并在提升之前预先找出会被拦住的内容,整理成报告。

为什么重要

PSA 不需要安装任何东西——API 服务器中已经启用,只需在命名空间上挂三个标签。所以它看起来很简单,但事故总是出在跳过顺序的时候。直接启用 enforce 的团队,当天部署就全被拦住;只开启 warn 的团队,没有人阅读警告,半年后又站到同一个地方。处于这两种失败之间的,就是本实验要讲的顺序——通过观测收集现状,固定等级的版本,把集群升级与策略变更分开,在切换之前预先找出现有 Pod 的违规,列出要修复的清单,然后再提升。而且要知道,提升之后,已经在运行的 Pod 是拦不住的,这样才不会把修改标签的那天误当成已经完成。

步骤

  1. 在 /root/psa/plain.yaml 中写一个 Pod——名称 web,标签 app: web,容器名称 web,镜像 nginx:1.27,securityContext 一行也不写。创建不带 PSA 标签的命名空间 psa-legacy,并应用这个 Pod。然后在 /root/psa/default.txt 中用一个词写出适用于没有任何标签的命名空间的 enforce 等级的名称。
  2. 创建命名空间 psa-observe,只加上 pod-security.kubernetes.io/warn=restricted 和 pod-security.kubernetes.io/audit=restricted 两个标签(不设置 enforce)。用 kubectl create -f plain.yaml -n psa-observe 放入同一个 plain.yaml,并把标准输出和标准错误一起保存到 /root/psa/warn.txt。该文件中必须同时有“Pod 已创建”这一行以及全部四条违规原因。
  3. 创建命名空间 psa-strict,并加上 pod-security.kubernetes.io/enforce=restricted。用 kubectl create -f plain.yaml -n psa-strict --dry-run=server 放入同一个 plain.yaml,把拒绝消息连同标准错误保存到 /root/psa/denied.txt。把该消息中写的四项全部修复后的 Pod 写入 /root/psa/hardened.yaml(名称 web-hardened,镜像仍为 nginx:1.27),并真正应用到 psa-strict。
  4. 创建命名空间 psa-baseline,并加上 pod-security.kubernetes.io/enforce=baseline。在 /root/psa/privileged.yaml 中写一个连 baseline 也违反的 Pod(名称 breaker,容器名称 breaker,镜像 busybox:1.36)。然后在 /root/psa/gap.txt 中,把三个清单(plain.yaml、hardened.yaml、privileged.yaml)分别放入两个命名空间的结果,以 <파일이름> baseline=<allow|deny> restricted=<allow|deny>(占位符为文件名)的形式每行一条写出。判定用 --dry-run=server 进行,不真正创建 Pod。
  5. 给三个命名空间的等级标签,分别加上与之配对的版本标签,值为 v1.30——psa-strict 和 psa-baseline 是 pod-security.kubernetes.io/enforce-version,psa-observe 是 pod-security.kubernetes.io/warn-version 和 pod-security.kubernetes.io/audit-version。然后创建 /root/psa/pinned.sh——不带参数运行时,只输出设置了 enforce、audit、warn 标签、却没有与之配对的 -version 标签的命名空间的名称,每行一个(排序后)。
  6. 先往 psa-legacy 里再放入两个 Pod——把 plain.yaml 只改名为 api 的,以及把 hardened.yaml 只改名为 batch 的。然后创建专门用于判定的命名空间 psa-canary,加上 pod-security.kubernetes.io/enforce=restricted 和 pod-security.kubernetes.io/enforce-version=v1.30。把 kubectl label ns psa-legacy pod-security.kubernetes.io/enforce=restricted --overwrite --dry-run=server 的输出连同标准错误保存到 /root/psa/preview.txt——标签不能真的被加上。最后创建 /root/psa/violators.sh <네임스페이스>(占位符为命名空间):把该命名空间的 Pod 逐个重新提交到 psa-canary(--dry-run=server),只把违反 restricted 的名称,每行一个、排序后输出。把 ./violators.sh psa-legacy 的结果保存到 /root/psa/violators.txt。
  7. 这次真正给 psa-legacy 加上 pod-security.kubernetes.io/enforce=restricted 和 pod-security.kubernetes.io/enforce-version=v1.30。然后把 hardened.yaml 应用到 psa-legacy(必须通过),用 kubectl create -n psa-legacy --dry-run=server 放入 plain.yaml,把拒绝消息连同标准错误保存到 /root/psa/enforced.txt,再把 kubectl get pod -n psa-legacy 的输出接在该文件之后。已经在运行的 web 和 api 不要删除。
  8. 在 /root/psa/targets.txt 中每行写一个要检查的命名空间名称——psa-legacy、psa-observe、psa-strict、psa-baseline 四个。创建 /root/psa/readiness.sh [목록파일](占位符为列表文件;不带参数时使用 /root/psa/targets.txt)。对列表中的每个命名空间输出一行判定:如果已经是 enforce=restricted,就是 <이름> ENFORCED;如果没有任何违规的 Pod,就是 <이름> READY;如果有,就是 <이름> BLOCKED <개수>;如果没有这样的命名空间,就是 <이름> MISSING(占位符依次为命名空间名称与数量)。不能真的修改标签。把运行结果保存到 /root/psa/report.txt。

参考

没有任何标签的命名空间,什么都不会拦

在 /root/psa/plain.yaml 中写一个 Pod——名称 web,标签 app: web,容器名称 web,镜像 nginx:1.27,securityContext 一行也不写。创建不带 PSA 标签的命名空间 psa-legacy,并应用这个 Pod。然后在 /root/psa/default.txt 中用一个词写出适用于没有任何标签的命名空间的 enforce 等级的名称。

PodSecurity 是 API 服务器中已经启用的准入控制器,按哪个等级来看,只由命名空间标签决定。没有标签时使用集群默认值,而这个默认值是什么都不拦的一侧。Pod Security Standards 有三个等级——其中最宽松的那个的名称。

只开启警告,同一个 Pod 在带有警告的情况下被创建了

创建命名空间 psa-observe,只加上 pod-security.kubernetes.io/warn=restricted 和 pod-security.kubernetes.io/audit=restricted 两个标签(不设置 enforce)。用 kubectl create -f plain.yaml -n psa-observe 放入同一个 plain.yaml,并把标准输出和标准错误一起保存到 /root/psa/warn.txt。该文件中必须同时有“Pod 已创建”这一行以及全部四条违规原因。

PSA 的三个模式彼此独立——只有 enforce 会拦截请求,warn 会把警告返回给发起请求的人,audit 则在审计日志中留下注解。警告是通过标准错误输出的,所以要像 > 파일 2>&1(韩文,意为“文件”)这样,把两者都接收下来。这一步的价值在于回答“既然什么都拦不住,为什么还要开启”。

读完四条原因,把 Pod 改成符合 restricted

创建命名空间 psa-strict,并加上 pod-security.kubernetes.io/enforce=restricted。用 kubectl create -f plain.yaml -n psa-strict --dry-run=server 放入同一个 plain.yaml,把拒绝消息连同标准错误保存到 /root/psa/denied.txt。把该消息中写的四项全部修复后的 Pod 写入 /root/psa/hardened.yaml(名称 web-hardened,镜像仍为 nginx:1.27),并真正应用到 psa-strict。

拒绝消息会原样告诉你要修复的位置——只需分清哪些属于 Pod 级别的 securityContext,哪些属于容器级别,再放进去即可。把 runAsNonRoot 设为 true,意味着镜像不以 root 运行,所以最好同时指定要运行的 UID。capabilities 的顺序是先全部丢弃,再只把需要的重新加回来。

baseline 能通过,只有 restricted 会拦住的 Pod

创建命名空间 psa-baseline,并加上 pod-security.kubernetes.io/enforce=baseline。在 /root/psa/privileged.yaml 中写一个连 baseline 也违反的 Pod(名称 breaker,容器名称 breaker,镜像 busybox:1.36)。然后在 /root/psa/gap.txt 中,把三个清单(plain.yaml、hardened.yaml、privileged.yaml)分别放入两个命名空间的结果,以 <파일이름> baseline=<allow|deny> restricted=<allow|deny>(占位符为文件名)的形式每行一条写出。判定用 --dry-run=server 进行,不真正创建 Pod。

baseline 只拦截广为人知的权限提升,restricted 则在此基础上再加上强化规则。两者都不能使用的是宿主机命名空间和特权容器。如果已经存在同名的 Pod,会在到达准入之前就以 AlreadyExists 结束,所以提交时改掉名称和命名空间再发送更安全(可以用 kubectl create -f 파일 --dry-run=client -o json(占位符为文件)取出,用 jq 修改后再传递)。

设为 latest 的命名空间,会在升级集群那天停摆

给三个命名空间的等级标签,分别加上与之配对的版本标签,值为 v1.30——psa-strict 和 psa-baseline 是 pod-security.kubernetes.io/enforce-version,psa-observe 是 pod-security.kubernetes.io/warn-version 和 pod-security.kubernetes.io/audit-version。然后创建 /root/psa/pinned.sh——不带参数运行时,只输出设置了 enforce、audit、warn 标签、却没有与之配对的 -version 标签的命名空间的名称,每行一个(排序后)。

如果不加版本标签,那个位置就按 latest 运行——意思是等级的“当前版本”,所以升级集群时规则也会一起升级,昨天还能通过的 Pod,今天就被拦住了。标签全都包含在 kubectl get ns -o json 的 .items[].metadata.labels 中,而对于没有任何标签的命名空间,那个位置根本不存在。这是一个一边遍历三个模式,一边查看配对标签是否存在的问题。

提升之前,预先找出会被拦住的内容

先往 psa-legacy 里再放入两个 Pod——把 plain.yaml 只改名为 api 的,以及把 hardened.yaml 只改名为 batch 的。然后创建专门用于判定的命名空间 psa-canary,加上 pod-security.kubernetes.io/enforce=restricted 和 pod-security.kubernetes.io/enforce-version=v1.30。把 kubectl label ns psa-legacy pod-security.kubernetes.io/enforce=restricted --overwrite --dry-run=server 的输出连同标准错误保存到 /root/psa/preview.txt——标签不能真的被加上。最后创建 /root/psa/violators.sh <네임스페이스>(占位符为命名空间):把该命名空间的 Pod 逐个重新提交到 psa-canary(--dry-run=server),只把违反 restricted 的名称,每行一个、排序后输出。把 ./violators.sh psa-legacy 的结果保存到 /root/psa/violators.txt。

预警在 Pod 很多时,会缩写成 (and N other pods)——要知道全部名称,就得逐个 Pod 分别判定。已经在运行的 Pod 不会再次经过准入,所以办法是把同样的 spec 重新提交到强制该等级的命名空间中。从 kubectl get pod <이름> -n <ns> -o json(占位符为 Pod 名称)中只提取 apiVersion、kind、spec,metadata 则重新构造即可。

提升之后,已经在运行的 Pod 仍然照常运行

这次真正给 psa-legacy 加上 pod-security.kubernetes.io/enforce=restricted 和 pod-security.kubernetes.io/enforce-version=v1.30。然后把 hardened.yaml 应用到 psa-legacy(必须通过),用 kubectl create -n psa-legacy --dry-run=server 放入 plain.yaml,把拒绝消息连同标准错误保存到 /root/psa/enforced.txt,再把 kubectl get pod -n psa-legacy 的输出接在该文件之后。已经在运行的 web 和 api 不要删除。

PSA 是准入控制器,只在请求进来时才做判断——已经保存的对象不会再看,所以即使有违规的 Pod 在运行,也不会把它赶走。因此刚提升到 enforce 的集群,是一种“只有新进来的才是干净的”状态,已有的 Pod 要等到重新部署时才会被拦住。如果不了解这个时间差,看到提升那天什么都没发生,就会错误地判断它是安全的。

整理成报告,说明可以先提升哪个命名空间

在 /root/psa/targets.txt 中每行写一个要检查的命名空间名称——psa-legacy、psa-observe、psa-strict、psa-baseline 四个。创建 /root/psa/readiness.sh [목록파일](占位符为列表文件;不带参数时使用 /root/psa/targets.txt)。对列表中的每个命名空间输出一行判定:如果已经是 enforce=restricted,就是 <이름> ENFORCED;如果没有任何违规的 Pod,就是 <이름> READY;如果有,就是 <이름> BLOCKED <개수>;如果没有这样的命名空间,就是 <이름> MISSING(占位符依次为命名空间名称与数量)。不能真的修改标签。把运行结果保存到 /root/psa/report.txt。

如果直接调用前一步的 violators.sh,数量就只需统计行数。之所以要先筛掉已经是 restricted 的地方,是因为对这样的命名空间以相同的值再次贴标签,值不会发生变化,预警根本不会出现。如果让列表文件作为参数传入,同一个脚本也可以用在其他批次上。