ValidatingAdmissionPolicy 与 CEL 原生准入工程
一个平台团队上线了“Deployment 必须标注 owner”的 CEL 策略,Policy 对象创建成功,测试 YAML 也能正常提交,于是变更被标记为完成。数周后,无 owner 的工作负载仍不断进入集群。排查发现从未创建 ValidatingAdmissionPolicyBinding:Policy 只定义规则,Binding 才把规则连接到请求并指定 Deny、Warn 或 Audit。团队验证的是“规则可以存储”,不是“规则正在执行”。
另一次故障更隐蔽。策略通过参数对象维护允许的 owner 后缀,参数被误删后,Binding 使用了 parameterNotFoundAction: Allow;同时一条表达式直接索引不存在的 label,failurePolicy: Ignore 又跳过了运行时错误。监控只统计拒绝数,因此图表一片平静。业务对象被放行可能来自未命中、参数缺失、表达式错误或规则正常返回 true,单看最终 HTTP 状态无法分辨这些路径。
原生 VAP 是三段式执行模型
ValidatingAdmissionPolicy 是 kube-apiserver 内建的验证型准入能力。它在 API Server 进程内运行 CEL,不需要部署 webhook Service、证书和策略控制器。Validating Admission Policy 说明给出了执行模型与稳定状态;对象使用 admissionregistration.k8s.io/v1,功能从 Kubernetes 1.30 起进入 stable。这里的 v1 是 API 版本,1.30 是功能成熟与最低稳定集群基线,两者不能混成“VAP 1.30 API”。
一个可执行策略由三个角色组成:Policy 描述匹配、变量、验证和执行错误语义;可选 Param 资源提供会变化的组织数据;Binding 激活 Policy、进一步收窄资源、选择参数并设置正常违规的动作。API Server 会对每个匹配的 (policy, binding, param) 组合求值,多 Binding 或多参数按 AND 收敛,任一组合拒绝都会拒绝整个请求。
failurePolicy、parameterNotFoundAction 与 validationActions 管的是三类不同事件。failurePolicy 处理类型、运行时、无效参数 GVK 等执行错误;parameterNotFoundAction 处理 Binding 没有找到参数;validationActions 处理表达式正常求值为 false。把三者都叫“失败时行为”,会让放行原因无法诊断。
先从服务端能力开始
VAP 没有 Helm Chart,也没有需要安装的 controller。目标是 Kubernetes 1.30 或更高的稳定集群时,直接检查服务端 discovery。不要用客户端版本、云控制台产品名称或某个测试集群的结果代替目标集群事实:
kubectl config current-context
kubectl version
kubectl api-resources --api-group=admissionregistration.k8s.io \
| grep -E 'validatingadmissionpolic(y|ies)|validatingadmissionpolicybinding'
kubectl explain validatingadmissionpolicy --api-version=admissionregistration.k8s.io/v1
kubectl explain validatingadmissionpolicybinding.spec.validationActions \
--api-version=admissionregistration.k8s.io/v1预期 discovery 同时列出 Policy 和 Binding,kubectl explain 能解析 v1 字段。若资源不存在,应先核对服务端 minor、发行版支持和托管集群能力;不要把旧集群上手工打开 feature gate 当作新的生产基线。Policy v1 API与 Binding v1 API适合在字段校验失败时逐项核对结构。若 API 存在但某个 CEL 函数创建失败,则要查阅 Kubernetes CEL 中函数的最低 minor:CEL 库随集群版本增加,稳定 VAP API 并不意味着所有新函数都能在较老 API Server 上使用。
执行下面的实验需要一个隔离 namespace,以及创建 CRD、参数对象、VAP 和 Binding 的权限。Binding 引用参数时,创建者还必须具有参数资源的读取权限。因为策略会影响后续 API 写入,先确认当前 context 与 namespace,并让恢复人员保留一条不依赖该策略的受控管理路径;公开仓库里不应放真实集群名、主体或应急凭据。
kubectl create namespace vap-lab
kubectl config set-context --current --namespace=vap-lab
kubectl auth can-i create validatingadmissionpolicies.admissionregistration.k8s.io
kubectl auth can-i create validatingadmissionpolicybindings.admissionregistration.k8s.io
kubectl auth can-i create customresourcedefinitions.apiextensions.k8s.io三个 can-i 都应返回 yes 才继续。它们只证明调用者当前获得相应授权,不证明策略语义正确,也不建议给日常开发者长期授予这些集群级权限。
用参数资源保存可变组织规则
把 owner 后缀直接写在 CEL 中,每次组织映射变化都要修改高风险 Policy。更稳妥的做法是让 Policy 固定算法,让 namespace 级参数资源保存允许后缀。先创建 CRD:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: deploymentownerpolicies.policy.example.com
spec:
group: policy.example.com
scope: Namespaced
names:
plural: deploymentownerpolicies
singular: deploymentownerpolicy
kind: DeploymentOwnerPolicy
shortNames: [dop]
versions:
- name: v1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required: [allowedSuffix]
properties:
allowedSuffix:
type: string
minLength: 2
---
apiVersion: policy.example.com/v1
kind: DeploymentOwnerPolicy
metadata:
name: team-owner-policy
namespace: vap-lab
spec:
allowedSuffix: "@example.com"保存为 owner-param.yaml 后执行:
kubectl apply -f owner-param.yaml
kubectl get deploymentownerpolicy team-owner-policy -n vap-lab -o yaml预期参数对象的 spec.allowedSuffix 为 @example.com。真实组织可以改用团队 ID、成本中心或受控枚举,避免把个人邮箱当成长期身份。参数本身是安全配置:谁能修改它,谁就能改变所有引用 Binding 的允许集合,因此应由独立 RBAC、代码评审和审计保护。
Policy 定义算法与执行错误
下面的 Policy 只匹配 apps/v1 Deployment 的 CREATE 与 UPDATE。第一条 matchCondition 排除实验策略自己的管理 namespace,第二条保留一个清晰的请求操作约束。variables 先安全提取 owner,避免直接索引不存在的 key;validation 再检查 owner 存在且以后缀结尾。
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: deployment-owner.example.com
spec:
failurePolicy: Fail
paramKind:
apiVersion: policy.example.com/v1
kind: DeploymentOwnerPolicy
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
matchConditions:
- name: outside-policy-system
expression: "request.namespace != 'policy-system'"
- name: write-operation
expression: "request.operation in ['CREATE', 'UPDATE']"
variables:
- name: owner
expression: >-
has(object.metadata.labels) && 'owner' in object.metadata.labels
? object.metadata.labels['owner']
: ''
validations:
- expression: >-
variables.owner != '' && variables.owner.endsWith(params.spec.allowedSuffix)
messageExpression: >-
variables.owner == ''
? 'Deployment 必须设置 metadata.labels.owner'
: 'owner 必须使用团队允许的后缀'
reason: Invalid
auditAnnotations:
- key: owner-policy
valueExpression: >-
'owner=' + (variables.owner == '' ? '<missing>' : variables.owner)matchConstraints 先按 API 资源缩小候选请求,Binding 还会再次缩小 namespace 或对象,matchConditions 才对请求上下文做 CEL 判断。条件任意一个为 false 就跳过 Policy;全部为 true 才继续;若有 error 且没有 false,failurePolicy: Fail 会拒绝,Ignore 会跳过。Kubernetes 限制每个 Policy 最多 64 个 match conditions,这不是鼓励堆满条件,而是提醒团队把粗粒度匹配留给结构化 selector,把需要解释的请求逻辑留给少量 CEL。
auditAnnotations 只有在 Policy 参与求值且 valueExpression 返回非空字符串时才有值。键会与 Policy 名共同形成 audit annotation 身份;值不应包含 token、完整请求体或个人敏感信息。它是否最终被保存,还取决于 kube-apiserver 的 audit policy 与后端配置,创建了 annotation 不能证明集群一定持久化了 audit event。
应用 Policy:
kubectl apply -f owner-policy.yaml
kubectl get validatingadmissionpolicy deployment-owner.example.com -o yaml预期对象创建成功。此刻规则仍未激活,这是一个重要中间状态,而不是缺陷。
Binding 把策略接到请求上
先用 Warn 与 Audit 接入实验 namespace,让违规请求成功但留下客户端与审计信号:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: deployment-owner-vap-lab
spec:
policyName: deployment-owner.example.com
paramRef:
name: team-owner-policy
namespace: vap-lab
parameterNotFoundAction: Deny
matchResources:
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: vap-lab
validationActions: [Warn, Audit]kubectl apply -f owner-binding.yaml
kubectl get validatingadmissionpolicybinding deployment-owner-vap-lab -o yamlpolicyName 指向算法,paramRef 选择参数,matchResources 定义本次激活范围,validationActions 决定正常违规如何呈现。parameterNotFoundAction: Deny 表示参数消失时拒绝请求,它不等于 CEL validation 返回 false。参数 selector 若命中多个对象,每个参数都会与 Policy、Binding 组成一次求值,结果按 AND 收敛;一个允许、一个拒绝时整体拒绝。多 Binding 同样如此,因此排障不能只看最熟悉的那一个。
Deny 与 Warn 不能同时设置;可以用 [Deny, Audit] 在阻断时继续产生审计 annotation。Warn 通过 HTTP warning header 返回给客户端,适合观察期,但调用方未必展示或保存 warning。Audit 不扫描存量资源。需要统计历史违规对象时,应运行独立清单扫描或选择带周期 audit/report 的策略平台。
正向实验:合法对象完整通过
准备 deployment-good.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: owner-good
namespace: vap-lab
labels:
owner: platform@example.com
spec:
replicas: 1
selector:
matchLabels:
app: owner-good
template:
metadata:
labels:
app: owner-good
owner: platform@example.com
spec:
containers:
- name: pause
image: registry.k8s.io/pause:3.10kubectl apply -f deployment-good.yaml
kubectl get deployment owner-good -n vap-lab \
-o jsonpath='{.metadata.labels.owner}{"\n"}'预期创建成功,输出 platform@example.com,客户端没有 owner validation warning。这个实验验证 Policy、Binding、Param 与匹配链能够产生允许结果;它仍不能证明反例会被发现,所以必须继续做反向实验。
反向实验一:观察与阻断是两步
准备缺少 owner 的 deployment-bad.yaml,其余字段与正例相同:
apiVersion: apps/v1
kind: Deployment
metadata:
name: owner-bad
namespace: vap-lab
spec:
replicas: 1
selector:
matchLabels:
app: owner-bad
template:
metadata:
labels:
app: owner-bad
spec:
containers:
- name: pause
image: registry.k8s.io/pause:3.10在 [Warn, Audit] 阶段执行:
kubectl apply -f deployment-bad.yaml
kubectl get deployment owner-bad -n vap-lab预期请求成功,客户端出现“Deployment 必须设置 metadata.labels.owner”相关 warning,对象确实存在。若集群 audit policy 记录该类事件,audit backend 中还应出现与 VAP validation failure 和 owner-policy 相关的 annotation;没有配置 audit backend 时,不应声称已获得审计证据。
确认 warning 的调用方覆盖率和现有违规后,把 Binding 改为:
spec:
validationActions: [Deny, Audit]删除刚才为观察创建的反例,再重试:
kubectl delete deployment owner-bad -n vap-lab --ignore-not-found
kubectl apply -f owner-binding.yaml
kubectl apply -f deployment-bad.yaml预期最后一条命令返回非零,API Status 的 message 包含 validation 的错误信息,owner-bad 不存在。这里写的是可复现实验的预期判据;只有在目标集群保存了命令输出、audit event 与对象状态后,团队才能把它记为一次实际验收。
反向实验二:参数缺失与表达式错误要分型
先删除参数对象:
kubectl delete deploymentownerpolicy team-owner-policy -n vap-lab
kubectl apply -f deployment-good.yaml在 parameterNotFoundAction: Deny 下,预期即使 owner 合法也被拒绝。把该字段临时改为 Allow 后,同一请求预期放行。这个 A/B 结果证明失败来自“没有参数”,而不是 owner validation。实验结束立即恢复 Deny 并重新创建参数;长期依赖 Allow 会把配置删除变成静默绕过。
表达式执行错误则由 failurePolicy 控制。可以在隔离 Policy 副本中把安全变量改成直接索引:
object.metadata.labels['owner'].endsWith(params.spec.allowedSuffix)对没有 labels 的 Deployment,这个表达式可能产生运行时错误。failurePolicy: Fail 时预期拒绝;改为 Ignore 时预期该 Policy 被跳过并放行。再提交一个 labels 存在但后缀错误的对象:它是正常 false,即使 failurePolicy: Ignore,仍应按 Binding 的 Deny 拒绝。这个对照能防止团队误以为 Ignore 会忽略所有违规。
不要在共享策略上临时制造错误。为故障实验复制一个名称、限定一个一次性 namespace,完成后删除 Binding 再删除 Policy。否则测试本身可能阻断其他发布。
项目接入要检查渲染结果
策略仓库可以把对象分成 crd/、params/、policies/、bindings/ 与 fixtures/。应用项目不复制策略,只在 Helm values、Kustomize patch 或 Deployment 模板中提供受控 owner 值。CI 应先渲染,再用 schema 校验和 fixture 检查最终对象;准入负责最后一道集群约束,不能替代提交前反馈。
policy-repo/
crd/deployment-owner-policy.yaml
params/team-a.yaml
policies/deployment-owner.yaml
bindings/team-a-observe.yaml
fixtures/good.yaml
fixtures/missing-owner.yaml
application-repo/
charts/service/values.yaml
charts/service/templates/deployment.yaml一个稳妥的发布顺序是 CRD、参数、Policy、观察 Binding、业务资源、阻断 Binding。回滚顺序相反:先把 Binding 退到 Warn/Audit 或删除 Binding,确认发布恢复,再处理 Policy、参数和 CRD。Binding 是最清晰的执行开关;直接删除 Policy 会丢失诊断上下文,直接删除 CRD 还可能连参数对象一起清理。
GitOps 接入时,Policy、Binding 与 Param 各自只有一个字段 owner。不要同时让平台控制器生成 Binding、应用仓库又以同名对象声明另一份 scope。使用 server-side apply 时保存 managedFields,在字段争用或持续漂移时先确认写入者,而不是反复强制覆盖。
故障证据从最终状态向前找
当“应该拒绝却放行”时,先确认请求是否真的到达目标集群与 namespace,再依次查看 Policy、所有指向它的 Binding、所有命中的参数和 audit event。高频原因包括没有 Binding、Binding selector 不命中、operation 或 GVK 不匹配、matchCondition 为 false、参数缺失被 Allow、表达式错误被 Ignore。请求成功只是这些分支的共同外观。
kubectl get validatingadmissionpolicy deployment-owner.example.com -o yaml
kubectl get validatingadmissionpolicybinding -o yaml
kubectl get deploymentownerpolicy -A -o yaml
kubectl get deployment <name> -n <namespace> -o yaml当“意外拒绝”时,列出所有匹配 Binding 和 selector 命中的全部参数,不要只改第一条规则。多组合 AND 收敛常使某个旧 Binding 继续拒绝。API Status 中的 reason/message、Policy 的 validation message、audit ID 与对象 GVK 是第一批证据;若 Policy 对象创建即失败,则属于 API schema、类型检查或静态 cost 问题,还没有进入业务对象的 admission runtime。
audit annotation 缺失时,先确认请求是否触发失败 validation、Binding 是否包含 Audit、annotation valueExpression 是否返回非空,再检查 API Server audit policy 与后端。VAP 不提供自己的报告控制器,也没有一个可查询的“历史违规列表”;把 kubectl get policy 当作违规报表会得到错误结论。
把性能预算留在控制平面
原生求值消除了 webhook 网络与证书故障,但每次匹配请求仍消耗 API Server CPU。性能治理首先靠减少候选请求:准确限制 apiGroups、apiVersions、operations 与 resources,用 namespace/object selector 做粗过滤,再用 matchConditions 表达无法结构化的请求条件。把所有资源都匹配后在 CEL 第一行判断 kind,会让无关请求也支付调度和求值成本。
CEL 有静态 cost estimation 与运行时 cost budget。对大型列表做嵌套 all、exists 或字符串正则,可能在 Policy 创建时被拒绝,也可能在特定大对象上触发运行时错误。variables 是按声明顺序、懒求值并记忆结果的,可复用昂贵中间结果,但不会把无限工作变便宜。性能实验应覆盖小对象、接近 schema 上限的大对象、批量创建和控制器高频更新,并观察 API Server admission 相关延迟、拒绝、错误与 CPU 趋势。
不要给所有集群套一个固定毫秒阈值。生产门槛应来自目标 API Server 容量、请求构成与发布 SLO。更有用的不变量是:引入策略后无关 GVK 的延迟分布不发生可解释范围外漂移;相同负载下 CEL error 与 cost-exceeded 为零;扩大 namespace 批次时尾延迟和 CPU 增量可预测;撤销 Binding 后指标回到稳定基线。
原生能力的安全边界
VAP 可以读取 object、oldObject、request、params、namespaceObject,在相应位置还可使用 authorizer 做 Kubernetes 授权检查。它不能任意列举集群对象,不能访问文件或网络,也没有 http.send。需要镜像漏洞库、任意 inventory、业务目录或外部风控结果时,应选择受控 webhook/策略平台,并把超时、缓存、TLS 与失败策略作为新的控制面依赖。
VAP 只验证 admission 请求,不能 mutation、generate、delete,也不周期扫描存量。它被 API 设计禁止匹配 VAP 与 VAPBinding 自身,以降低自锁风险,但这不代表管理对象天然安全;仍要用 RBAC、审计和变更评审保护策略 API。它也不能替代 Pod Security、镜像信任、运行时安全或应用内授权。
把 request.userInfo 写进规则前要考虑调用主体:GitOps controller、Deployment controller 与人类操作员可能代表不同身份,同一个对象由不同控制器更新时会得到不同结果。主体相关规则通常不适合迁移成存量扫描,也容易阻断系统控制器;应优先把稳定约束放在对象状态,把紧急主体例外交给受审计、短时、自动到期的控制流程,而不是在公开策略中留下永久绕过值。
从旧 webhook 迁移而不双重阻断
迁移先把旧规则整理成 fixture 契约:合法、缺字段、错误值、更新旧对象、参数缺失、执行器错误各一组,并记录旧执行器的 allow/deny/reason。用 VAP 表达能够原生完成的确定性验证;依赖 mutation、外部数据、跨对象 inventory 或存量 audit 的部分继续留在对应平台,不要为了“全原生”削弱语义。
新 VAP 先以 [Warn, Audit] 运行,旧 webhook 保持唯一 Deny。比较两边命中集合、scope、参数、错误消息和耗时,修正差异后把 VAP 改成 [Deny, Audit],同时把旧规则降为 warn/dry-run。经过完整发布周期与故障演练后,再删除旧规则;最后才考虑缩减 webhook 控制器。双轨期间若两个执行器都 Deny,客户端通常只能看到先返回的错误,拒绝次数和延迟也会重复,反而降低证据质量。
升级 Kubernetes 前,扫描 Policy 使用的 CEL 函数和库,确保回滚目标 minor 仍支持。较新 API Server 接受的新表达式,回滚到较旧控制平面后可能只能继续求值已有对象,却不能重新创建或更新同样策略;因此要在回滚版本的隔离集群验证“导出后可重建”,不能只验证原地对象仍存在。
清理实验与保留证据
清理时先移除执行点,避免删除参数后触发 parameterNotFoundAction: Deny 阻断仍匹配的请求:
kubectl delete validatingadmissionpolicybinding deployment-owner-vap-lab
kubectl delete validatingadmissionpolicy deployment-owner.example.com
kubectl delete deployment owner-good owner-bad -n vap-lab --ignore-not-found
kubectl delete deploymentownerpolicy team-owner-policy -n vap-lab --ignore-not-found
kubectl delete crd deploymentownerpolicies.policy.example.com
kubectl delete namespace vap-lab团队环境不要把 audit event、拒绝 Status 和策略 revision 与实验 namespace 一起立即销毁。先按审计保留策略导出脱敏证据,确认没有其他 Binding 引用 Policy、没有其他参数实例依赖 CRD,再执行清理。长期退出的完成标准不是 kubectl delete 返回成功,而是执行点消失、目标请求恢复、策略与参数所有权归还、审计记录可追溯、控制平面延迟回到基线,并且旧 webhook 或生成控制器不再重建同名对象。
VAP 最有价值的地方不是“少装一个控制器”,而是把确定性验证缩短为 API Server 内的一条可审计路径。要得到这项收益,Policy、Binding、Param、匹配、动作和错误必须分别建模;观察、阻断与存量治理必须分别取证;性能成本、回滚开关和迁移 owner 必须在第一条 Deny 生效前就准备好。
