有创建 Pod 的权限,但特权 Pod 必须被拦下
目标
用 ValidatingAdmissionPolicy(VAP),亲手以 CEL 编写“禁止特权容器”和“禁止 hostPath 卷”这两条策略,通过绑定只在一个命名空间里启用,然后观察 API 服务器返回的拒绝消息和正常 Pod 被放行的情况,两边都要看。
为什么重要
Kubernetes 的所有写入请求,都必须经过认证 → 授权(RBAC)→ 准入,才能存入 etcd。RBAC 只回答到“这个用户能不能创建 Pod”,并不会问“那个 Pod 的 内容 安全吗”。只要有创建 Pod 的权限,就可以用 privileged: true,或者把节点的 / 作为 hostPath 挂载的 Pod,把节点整个拿下。堵住这个漏洞的,就是准入控制这一层。
Pod Security Admission(PSA)靠一个命名空间标签,就能打开规定好的标准(privileged/baseline/restricted)。它很方便,但无法表达组织自己的规则(允许的注册表、必须有某个标签等)。VAP 在 API 服务器内部直接求值 CEL 表达式,所以不需要 Webhook 服务器也能写出想要的规则,而且 策略(检查什么)和 绑定(在哪里,以 Deny/Warn/Audit 中的哪一种)是分开的,所以同一条策略可以在各个命名空间里以不同方式启用。
拒绝消息里会以名称标出是哪条策略、哪个绑定拦住了它。事故响应时,最快解开“为什么部署不了”的线索,就是这一行。还要记住,准入只发生在 创建请求的时刻,所以在启用策略之前就已经创建的对象,会原样保留。
步骤
- 创建命名空间
kcsa-adm,并确认自动标签。 - 用 CEL 编写拒绝特权容器的策略
deny-privileged。 - 用绑定
deny-privileged-binding,只在kcsa-adm中以 Deny 方式启用。 - 试着创建特权 Pod,把拒绝消息保存到
/root/kcsa-adm/denied-privileged.txt。 - 确认普通 Pod
good被放行。 - 创建拒绝 hostPath 卷的策略
deny-hostpath和绑定。 - 试着创建 hostPath Pod,把拒绝消息保存到
/root/kcsa-adm/denied-hostpath.txt。 - 把什么被拦住、什么通过了,留在
/root/kcsa-adm/report.txt中。
参考
- 只创建策略而漏掉绑定,什么都拦不住。请先用
kubectl get validatingadmissionpolicybinding看两者是否都在。 - 把
validationActions设为Warn,请求会通过,只出现警告。要拦住,必须是Deny。 - 这个实验里的 Pod 没有容器,只是模拟,但准入发生在 API 请求阶段,所以拒绝和放行是真的。
- 官方文档:Validating Admission Policy · Pod Security Standards.
创建要设置策略的隔离区域
创建命名空间 kcsa-adm。API 服务器会自动给这个命名空间加上 kubernetes.io/metadata.name=kcsa-adm 标签,后面步骤的绑定就用这个标签来选择对象。
Kubernetes 会给所有命名空间自动加上以自己的名字为值的 kubernetes.io/metadata.name 标签。创建之后,用 kubectl get ns <이름> --show-labels(占位符为名称)确认一下。把 create 用 --dry-run=client 生成后再 apply,运行多次也安全。
用 CEL 写出拦住特权容器的规则
创建 ValidatingAdmissionPolicy deny-privileged。写一个 CEL 表达式,在 Pod CREATE 请求中,只要有一个容器的 securityContext.privileged 是 true 就拒绝,拒绝消息设为 privileged containers are not allowed。failurePolicy 为 Fail。
策略用 matchConstraints.resourceRules 决定看哪类请求(核心组 v1 pods,CREATE),validations[].expression 必须为真才能通过。像 object.spec.containers.all(c, ...) 这样询问所有容器是否都满足条件,但完全没有 securityContext 或 privileged 字段的情况也必须算作通过,所以要先用 has() 确认。只靠策略,什么都拦不住。
把规则绑定到 kcsa-adm,真正启用
创建 ValidatingAdmissionPolicyBinding deny-privileged-binding。policyName 是 deny-privileged,validationActions 是 ["Deny"],对象用 matchResources.namespaceSelector 的 matchLabels 只选出 kubernetes.io/metadata.name: kcsa-adm 的命名空间。
VAP 把策略(检查什么)和绑定(在哪里、以什么动作)分开。没有绑定,策略连求值都不会发生。validationActions 里除了 Deny 还有 Warn、Audit,必须是 Deny,请求才会真正被拒绝。选择命名空间时,使用第 1 步确认的自动标签。
特权 Pod 被挡在门口
试着在命名空间 kcsa-adm 中创建一个带有 securityContext.privileged: true 容器的 Pod(镜像 nginx:1.27-alpine),把 API 服务器返回的 完整拒绝消息 保存到 /root/kcsa-adm/denied-privileged.txt。
拒绝发生在 Pod 创建之前,所以 kubectl 会以非 0 的退出码,把错误输出到标准错误。标准错误也必须存入文件(2>&1)。消息里以名称写明了是哪条策略、哪个绑定拒绝的。刚创建绑定后,可能要过 1–2 秒才生效,所以如果 Pod 已经被创建出来,就把它删掉再重试。
普通 Pod 毫无阻碍地进入
在命名空间 kcsa-adm 中创建一个没有特权设置的普通 Pod good(镜像 nginx:1.27-alpine)。即使策略已启用,这个 Pod 也必须被放行。
好的策略只拦住坏请求,正常请求原样放行。CEL 表达式里有没有把没有 securityContext 的容器当作通过,就会在这里显现出来。如果创建成功,可以用 kubectl get pod good -n kcsa-adm 看到;如果被拦住,会出现与第 4 步形式相同的错误。
也要拦住通向节点磁盘的 hostPath
创建 ValidatingAdmissionPolicy deny-hostpath 和绑定 deny-hostpath-binding。策略在 Pod CREATE 时,只要有一个 hostPath 卷就拒绝(消息为 hostPath volumes are not allowed,failurePolicy: Fail),绑定以 validationActions 为 ["Deny"],只应用到 kubernetes.io/metadata.name: kcsa-adm 命名空间。
结构与第 2、3 步相同,只是检查对象变成了 volumes。有的 Pod 完全没有 volumes 字段,所以要先用 has(object.spec.volumes) 确认,再用 has() 询问每个卷有没有 hostPath 字段。把策略和绑定用 --- 连起来一次 apply 也可以。
想挂载节点 /etc 的 Pod 被拒绝
试着在命名空间 kcsa-adm 中创建一个把节点的 /etc 以 hostPath 卷挂载的 Pod(镜像 nginx:1.27-alpine),把完整的拒绝消息保存到 /root/kcsa-adm/denied-hostpath.txt。
形式是在 Pod spec 的 volumes 里放一个 hostPath 卷,再用容器的 volumeMounts 挂上去。像第 4 步那样,把标准错误存入文件。请确认这次消息里出现的策略和绑定的名称与第 4 步不同。
把什么被拦住、什么通过了,以账簿的形式留下
把观察结果写成四行保存到 /root/kcsa-adm/report.txt——privileged=denied、hostpath=denied、plain=allowed、enforcement=validatingadmissionpolicy。评分器会在真实集群里创建 Pod,逐行核对。
值是 denied 或 allowed 二选一,最后一行用小写的一个单词写下是谁执行了这个拒绝(不是 PSA 标签,而是哪种准入机制)。不要靠猜测,而要把第 4、5、7 步里亲眼看到的结果抄进来。