Kubernetes 准入控制实战:用 ValidatingAdmissionPolicy 守好集群边界

在 Kubernetes 准入控制体系中,ValidatingAdmissionPolicy 是从 1.30 起正式 GA 的“原生策略”能力:它用 CEL 表达式在请求写入 etcd 之前拦截并校验,无需再为 OPA/Kyvernus 部署额外 Webhook。本文用四个生产级示例,带你把集群的边界真正守起来。

一、为什么需要准入控制

RBAC 管的是“谁能调哪个 API”,而准入控制管的是“请求长什么样才算合法”。一个拥有 create pod 权限的用户,依然可能提交一个 privileged: true 的特权容器、一个没有资源限制的“饿死邻居”Pod,或来自未知仓库的镜像。这类风险 RBAC 看不住,只能靠准入层在请求抵达 etcd 前拦下来。关于 Pod/Deployment 的基础对象模型,可回顾 Kubernetes 入门:Pod、Deployment、Service。

过去做这件事几乎只有一条路:写(或引入)一个 ValidatingWebhook,自己起服务、管证书、扛可用性。Kubernetes 1.30 之后,ValidatingAdmissionPolicy 把最常用的“字段校验”沉淀成控制面原生能力,用 CEL 表达规则,零额外组件。

二、ValidatingAdmissionPolicy 是什么

它由两条资源组成:ValidatingAdmissionPolicy 定义“校验什么、怎么校验”,ValidatingAdmissionPolicyBinding 定义“对谁生效”。两者解耦,意味着一条策略可以被多个 Binding 复用(比如“禁止特权容器”同时绑定到业务命名空间和预发命名空间)。

2.1 CEL:策略就是一行表达式

CEL(Common Expression Language)是一种安全、无副作用的表达式语言,Kubernetes 把请求对象暴露为 request.object,你写一段布尔表达式,返回 true 即通过。它不支持循环外的副作用,因此天然比 Webhook 轻、比 Rego 好上手。

2.2 匹配规则 matchConstraints

用 matchConstraints.resourceRules 声明对哪些 Group/Version/Resource、哪些操作(CREATE/UPDATE)生效,避免策略误伤系统组件。

2.3 审计还是强制

Binding 的 validationActions 决定动作:["Deny"] 直接拒绝;["Warn","Audit"] 只告警并记录、不阻断。新策略强烈建议先 Audit 观察再切 Deny。

三、实战一:禁止特权容器

最经典的一条规则。注意 CEL 里要用 has() 判断字段是否存在,避免 securityContext 未设置时访问字段报错。

# no-privileged.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: no-privileged
spec:
  matchConstraints:
    resourceRules:
    - apiGroups: ["*"]
      apiVersions: ["*"]
      operations: ["CREATE", "UPDATE"]
      resources: ["pods"]
  validations:
  - expression: >-
      request.object.spec.containers.all(c,
        !has(c.securityContext.privileged) || c.securityContext.privileged != true)
    message: "Privileged containers are not allowed."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: no-privileged-binding
spec:
  policyName: no-privileged
  validationActions: ["Deny"]
  matchResources:
    namespaceSelector:
      matchLabels:
        env: prod

四、实战二:限制镜像只能来自内网仓库

供应链安全的第一道闸:只允许来自内部 registry 的镜像,杜绝开发者随手 docker run 来的未知镜像。镜像建议统一由 Docker 编排 流水线构建并推送到内网仓库。

# only-internal-registry.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: only-internal-registry
spec:
  matchConstraints:
    resourceRules:
    - apiGroups: ["*"]
      apiVersions: ["*"]
      operations: ["CREATE", "UPDATE"]
      resources: ["pods"]
  validations:
  - expression: >-
      request.object.spec.containers.all(c,
        c.image.startsWith("registry.internal.example.com/"))
    message: "Only images from registry.internal.example.com are allowed."

五、实战三:强制每个容器都设资源 requests/limits

没有资源限制的 Pod 会触发节点资源抢占,是线上“邻居雪崩”的常见根因。用一条策略强制 CPU/内存的 requests 与 limits 全部存在。

# require-resources.yaml
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: require-resources
spec:
  matchConstraints:
    resourceRules:
    - apiGroups: ["*"]
      apiVersions: ["*"]
      operations: ["CREATE", "UPDATE"]
      resources: ["pods"]
  validations:
  - expression: >-
      request.object.spec.containers.all(c,
        has(c.resources.requests.cpu) && has(c.resources.requests.memory) &&
        has(c.resources.limits.cpu) && has(c.resources.limits.memory))
    message: "Every container must set cpu/memory requests and limits."

六、实战四:按命名空间放行

策略与 Binding 解耦的价值在这里体现:同一条“禁止特权容器”,可以只绑定到带 env=prod 标签的命名空间,而 pre 环境放开做实验。再结合 excludedNamespaces 把 kube-system 等系统命名空间排除,避免误伤。

# binding 排除系统命名空间
spec:
  policyName: no-privileged
  validationActions: ["Deny"]
  matchResources:
    excludedNamespaces: ["kube-system", "kube-public"]
    namespaceSelector:
      matchLabels:
        env: prod

七、与 OPA、Kyverno、Webhook 怎么选

原生策略不是要取代一切,而是覆盖“80% 的简单校验”。复杂策略(需要查外部数据、模板复用、跨资源关联)仍适合 OPA/Kyverno。

方案额外部署策略语言适合场景
ValidatingAdmissionPolicy否(控制面内置)CEL常规字段校验、替代简单 Webhook
OPA Gatekeeper是Rego复杂策略、模板复用、外部数据
Kyverno是类 YAML 策略不想学 Rego、策略即 YAML
自定义 Webhook是任意需调外部系统/自研逻辑

八、生产落地检查清单

检查项建议
特性门(<1.30)开启 ValidatingAdmissionPolicy 特性门
先 Audit 后 Deny新策略先用 [“Audit”,”Warn”] 观察再切 Deny
Binding 范围用 namespaceSelector 限定生效命名空间
排除系统组件excludedNamespaces 排除 kube-system
失败策略failurePolicy 设 Fail,避免漏放
版本兼容确认 kubectl/控制面 ≥ 1.30

九、上线前必做的排错

策略不生效多半是 Binding 没匹配到、或 CEL 表达式对字段判断少了 has()。用下面几条命令自测:

# 查看策略与绑定是否就绪
kubectl get validatingadmissionpolicy
kubectl get validatingadmissionpolicybinding

# 看某条策略的详情与状态
kubectl describe validatingadmissionpolicy no-privileged

# 故意提交一个特权 Pod,观察拒绝信息
kubectl apply -f privileged-pod.yaml
# Error from server: admission webhook "..." denied the request: Privileged containers are not allowed.

建议把这些 YAML 纳入 Git,配合 ArgoCD GitOps 做版本化管理,并在 GitHub Actions 的 CI 里对 YAML 做基本 lint,避免策略随手写随丢。需要更细的网络层隔离时,可叠加 Cilium 的 eBPF 网络策略。

十、小结

ValidatingAdmissionPolicy 用控制面内置的 CEL,把“禁特权容器、限镜像来源、强制资源限制”这类高频校验做到了零额外组件、低延迟、易版本化。它是 Kubernetes 准入控制从“自建 Webhook”走向“平台原生”的关键一步:先用它覆盖 80% 的常规规则,把 OPA/Kyverno 留给真正复杂的场景,集群边界就能既稳又轻。

上一篇 Pulumi 实战:用 TypeScript 定义云基础设施
下一篇 TanStack Query 实战:前端数据请求、缓存与乐观更新
最新评论
Docker 网络模式详解:bridge/host 生产选型 - FSData
Docker 网络模式详解:bridge/host 生产选型 - FSData
8月20日
[…] 注意最后那条:把端口绑定到 127.0.0.1,再由宿主机上的 Nginx 反代进来,比直接 -p 80:80 暴露安全得多,也方便统一做
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
Vue3 + TypeScript 工程化:类型安全的前端架构 - FSData
8月20日
[…] Pinia 的工程化用法,可以参考我们之前的实战文章 《Vue3 组合式 API + Pinia:状态管理从入门到实战》,那里覆盖了 Sto
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
2026 年最值得装的 10 个 VS Code 插件(AI/前端/后端/效率全覆盖) - FSData
8月19日
[…] 状态,点一下就能起停容器,配合我们讲过的 Docker Compose 编排 […]
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
Redis 缓存设计:穿透、击穿、雪崩与最佳实践 - FSData
8月19日
[…] 缓存三大坑的本质都是”缓存失效导致请求穿透到数据库”。记住口诀:穿透用布隆、击穿用锁、雪崩用错峰。配合合理的过期策略
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
GitHub Actions 实战:从零搭建 CI/CD 流水线 - FSData
8月19日
[…] Docker 镜像部署。结合我们之前写的 Docker Compose 一键编排实战,可以在 CI […]