Kyverno:从 CEL 准入到资源协调的策略工程
一个平台团队把“工作负载必须声明 owner”写成 Kyverno 策略,先以 Audit 上线。新建 Deployment 能在报告里看到失败,团队便认为存量治理也已经接通。后来一次权限审计发现,很多早已存在的对象从未进入报告:策略级 background 被关闭,reports controller 也没有完成扫描。准入请求的 AdmissionReport、周期扫描产生的 BackgroundScanReport 和最终聚合的 PolicyReport 是三段证据,不是看到其中一段就能推断另外两段正常。
另一支团队把旧 ClusterPolicy 的 apiVersion 改成 policies.kyverno.io/v1 后直接发布。旧对象里 validate、mutate、generate 和 verifyImages 混在有序 rules 中,新 CEL API 却把这些动作拆成不同资源和控制器;旧 deny 条件迁移为 CEL 时还忘了反转为“允许条件”。结果验证策略放行了反例,后台控制器又持续修改存量对象。迁移失败的根因不是 YAML 拼写,而是执行模型、字段所有权和失败语义都变了。
先认清五类策略、四个控制器与选型边界
Kyverno 把策略写成 Kubernetes 资源,由控制器读取并接入 API Server 准入链。对新项目,稳定主线是 policies.kyverno.io/v1:ValidatingPolicy、MutatingPolicy、GeneratingPolicy、DeletingPolicy 和 ImageValidatingPolicy 分别承担验证、修改、生成、定时删除和镜像证据验证。它们都使用 CEL,但动作和故障半径完全不同。策略类型说明会列出各 API 的成熟状态与 legacy 类型的演进计划,升级前应以目标 release 的页面为准。
| 策略对象 | 改变什么 | 主要执行面 | 最需要防住的错误 |
|---|---|---|---|
ValidatingPolicy | 允许、警告、审计或拒绝请求 | admission、reports、CLI | 把表达式错误当成正常违规,把报告当成阻断 |
MutatingPolicy | 修改新请求,可选异步修改存量对象 | admission、background | 字段所有权争用、自触发更新、批量写放大 |
GeneratingPolicy | 按 trigger 创建或克隆 downstream | background | source、trigger、policy 删除后的生命周期不明 |
DeletingPolicy | 按 schedule 删除匹配对象 | cleanup | selector 过宽、传播策略错误、权限爆炸半径过大 |
ImageValidatingPolicy | 校验签名和 attestation | admission、reports | Registry、证书、缓存和失败策略让准入不可用 |
旧 Policy/ClusterPolicy 把 validate、mutate、generate、verifyImages 放进一组有序 rules,旧 CleanupPolicy/ClusterCleanupPolicy 另成一族。它们已经进入弃用路线,但“计划移除”不等于已经从当前集群消失。存量策略应按目标 Kyverno 版本核对 release notes,用 fixture 双轨比较后迁移;新策略不要继续积累 legacy 依赖。
运行时也不是一个“大控制器”。admission controller 承接 AdmissionReview;background controller 处理 generate 和 mutate-existing;reports controller 聚合准入结果并扫描存量资源;cleanup controller 执行定时删除。后面排障时,必须先问清哪一个执行面失效。
在一次性集群里固定安装基线
准入控制器会影响整个 API 写入链,实验应放在可销毁的 kind、k3d 或专用测试集群。先核对当前 context、Kubernetes 服务端版本、Helm 版本和权限;Kyverno release 页面给出的 Kubernetes 测试矩阵比 Chart 的 kubeVersion 约束更有意义,Helm 允许安装不代表该组合已经获得兼容承诺。
kubectl config current-context
kubectl version
helm version
kubectl auth can-i create customresourcedefinitions.apiextensions.k8s.io
kubectl auth can-i create validatingwebhookconfigurations.admissionregistration.k8s.io两项授权应返回 yes 才继续。创建 CRD 和 webhook configuration 都是集群级高权限,不应授予普通应用流水线。安装时先从 Kyverno releases 选出同时支持目标 Kubernetes minor 的 Kyverno release,再固定 Chart 版本。下面以 Kyverno v1.18.2 对应的 Chart 3.8.2 演示固定方式;如果目标环境已经采用更新 release,应同时替换版本并重新检查 values、CRD 和兼容矩阵。
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm show chart kyverno/kyverno --version 3.8.2
helm show values kyverno/kyverno --version 3.8.2 > kyverno-values-reference.yaml
helm upgrade --install kyverno kyverno/kyverno \
--namespace kyverno \
--create-namespace \
--version 3.8.2 \
--waithelm show chart 应显示目标 Chart 与 appVersion,--wait 成功后再检查四个 Deployment、Pod、Service endpoints、CRD 和 webhook:
helm status kyverno -n kyverno
kubectl get deployments,pods,services,endpointslices -n kyverno
kubectl api-resources --api-group=policies.kyverno.io
kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations \
-l app.kubernetes.io/part-of=kyverno预期 API discovery 至少能找到五类 CEL 策略对象,Deployment 可用副本达到期望,webhook 的 Service 能对应到 Ready endpoints。只看到 Pod Running 还不够:CRD 未建立、webhook 没有 endpoint、CA bundle 失效或 rules 没覆盖目标资源,都会让策略对象或准入链失效。
生产 values 不应直接复制实验命令。先用 helm show values 核对目标 Chart 的字段,再分别设置 admission、background、reports、cleanup 的副本、资源、PDB、拓扑分散和日志级别。admission 的普通请求可由多个副本承载;background 和 reports 的关键工作依赖 leader election,多副本首先提升接管能力,不代表协调吞吐线性增长。高可用指南适合用来设计故障域、优雅终止和恢复验证。
用 Audit 跑通第一条 CEL 策略
下面用隔离 namespace 验证 Deployment 必须携带非空 owner 标签。先创建实验空间和两个 fixture:
kubectl create namespace kyverno-lab保存 require-owner.yaml:
apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: require-deployment-owner
spec:
failurePolicy: Fail
validationActions:
- Audit
evaluation:
admission:
enabled: true
background:
enabled: true
webhookConfiguration:
timeoutSeconds: 5
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
matchConditions:
- name: lab-only
expression: "object.metadata.namespace == 'kyverno-lab'"
variables:
- name: owner
expression: "object.metadata.?labels['owner'].orValue('')"
validations:
- expression: "variables.owner != ''"
message: "Deployment 必须设置非空 metadata.labels.owner"
auditAnnotations:
- key: policy-owner
valueExpression: "variables.owner == '' ? '<missing>' : variables.owner"validationActions 处理表达式正常返回 false 时的动作;failurePolicy 处理表达式报错、webhook 超时或调用异常。两者不能互换。evaluation.admission.enabled 控制新请求,evaluation.background.enabled 控制这条策略是否参加存量报告扫描;timeoutSeconds 会进入生成的 webhook 配置,调大它会直接占用 kube-apiserver 的准入延迟预算。
应用策略并观察对象状态与 webhook rules:
kubectl apply -f require-owner.yaml
kubectl get validatingpolicy require-deployment-owner -o yaml
kubectl get validatingwebhookconfiguration -l app.kubernetes.io/part-of=kyverno -o yaml预期策略对象可读,status 中没有持续错误,动态 webhook rules 覆盖 apps/v1 Deployment。Kyverno 默认按现有策略收窄 webhook 匹配集合;排障不能只看策略 YAML,还要确认 API Server 实际会不会把该请求发送给 Kyverno。
准备 good.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: owner-good
namespace: kyverno-lab
labels:
owner: platform-team
spec:
replicas: 1
selector:
matchLabels:
app: owner-good
template:
metadata:
labels:
app: owner-good
spec:
containers:
- name: pause
image: registry.k8s.io/pause:3.10准备 bad.yaml,只把 metadata 下的 owner 删除,其余保持相同并改名为 owner-bad。然后执行:
kubectl apply -f good.yaml
kubectl apply -f bad.yaml
kubectl get deployments -n kyverno-lab
kubectl get admissionreports,clusteradmissionreports -A
kubectl get policyreports,clusterpolicyreports -A在 Audit 阶段,正例和反例都应创建成功;报告中正例应是 pass,反例应出现与 require-deployment-owner 对应的 fail。不同 report 中间对象可能被控制器聚合和清理,长期证据应以最终 PolicyReport、策略 revision、resource UID、controller 日志和 Kubernetes audit ID 关联,不能依赖某个短生命周期对象永远存在。
反向实验:正常违规与执行故障必须分型
确认报告链能够看到反例后,把 validationActions 改为 Deny,重新应用策略,删除反例并再次创建:
kubectl delete deployment owner-bad -n kyverno-lab --ignore-not-found
kubectl apply -f require-owner.yaml
kubectl apply -f bad.yaml
kubectl get deployment owner-bad -n kyverno-lab预期 kubectl apply 返回非零,Status message 包含 owner 约束,随后 get 返回 NotFound。这个结果证明正常 false 会被 Deny;它没有证明 webhook 不可达时如何处理。
执行错误要用隔离副本制造,不能破坏共享策略。复制策略,改名为 require-owner-error-lab,继续只匹配 kyverno-lab,再把变量故意改成直接索引不存在字段:
object.metadata.labels['owner']用一个完全没有 labels 的 Deployment 触发。failurePolicy: Fail 时预期请求失败,并在 admission controller 日志看到 CEL 求值错误;临时改为 Ignore 后,同一请求预期被这条策略跳过。随后提交一个 labels 存在但 owner 为空的对象:这是表达式正常返回 false,即使 failurePolicy 为 Ignore,仍应按 validationActions: [Deny] 拒绝。A/B 对照把“执行器坏了”和“资源违规”分开,避免把 fail-open 错当成关闭所有约束。
kubectl logs -n kyverno deployment/kyverno-admission-controller --since=10m
kubectl get events -n kyverno-lab --sort-by=.lastTimestamp
kubectl get validatingpolicy require-owner-error-lab -o yaml实验结束先删除错误策略。日志可能含资源名称、namespace、镜像和表达式上下文,导出前要脱敏;不要为了排障长期打开包含完整 AdmissionReview 的高详细度日志。
把存量报告和主动协调分开
reports controller 的 background scan 会周期读取现存资源并生成 BackgroundScanReport,再聚合为 PolicyReport。它不会因为发现 fail 就删除、回滚或修改对象。要验证这条链,先在 Audit 状态创建违规 Deployment,等待一次扫描窗口,再检查:
kubectl get backgroundscanreports,clusterbackgroundscanreports -A
kubectl get policyreports,clusterpolicyreports -A -o yaml
kubectl logs -n kyverno deployment/kyverno-reports-controller --since=30m预期报告包含目标 resource UID 与 fail 结果。若 admission 报告正常而存量报告为空,依次检查策略的 evaluation.background.enabled、reports controller 参数、leader、RBAC、队列和 PolicyReport CRD。全局关闭 backgroundScan 只会停掉报告扫描,不会停止 background controller 的 generate 或 mutate-existing;这两个开关名称相近,运行对象不同。
MutatingPolicy 的 admission mutation 与 mutate-existing 也必须拆开验证。前者在请求进入 etcd 前生成 patch,后者由 background controller 异步更新已经存在的目标。启用 evaluation.mutateExisting.enabled 后,要给 background controller ServiceAccount 对目标 GVK 的 get/list/watch/patch/update 权限;权限撤销时应看到策略创建检查、UpdateRequest、Event 或 controller 日志明确失败,而不是把对象没有变化解释成“规则没命中”。
生成策略同样有自己的生命周期。GeneratingPolicy 可以从 trigger 生成或克隆 downstream,evaluation.synchronize.enabled 决定后续协调,generateExisting 影响既有 trigger,orphanDownstreamOnPolicyDelete 影响删除 policy 后是否保留下游。正式采用前至少做四组删除实验:删除 downstream、删除 trigger、删除 source、删除 policy,并分别记录 synchronize 与 orphan 的组合结果。没有这张生命周期矩阵,回滚时就不知道删除控制面对象会不会连业务资源一起带走。
DeletingPolicy 更不是“另一种报告”。cleanup controller 会按 schedule 发起真实 Kubernetes DELETE,并受 deletionPropagationPolicy 影响。第一次实验只能匹配带一次性标签的 ConfigMap,先确认 selector、schedule、Event、RBAC 和传播结果,再逐步扩大对象类型。删除型策略的变更应要求双人评审、隔离环境 fixture 和自动到期;一个拼错的 namespace selector 足以把策略从测试空间扩展到全局。
镜像验证是一条外部依赖链
ImageValidatingPolicy 承接旧 verifyImages 的新 CEL 路径,但它不只是对镜像字符串做正则。真实请求可能需要访问 Registry、解析 digest、验证签名与 attestation、读取证书或信任根,并受缓存、代理、CA、网络出口和凭证影响。任何外部调用都会进入准入时延和失败策略,必须分别压测缓存命中、冷缓存、Registry 超时、凭证失效和证书轮换。
镜像策略只判断给定制品证据是否满足规则,不负责生成 SBOM、签名制品、保护构建身份或维护透明日志。供应链证据的签发和可信发布应由对应工具链完成,Kyverno 在准入点消费已定义的证据。Registry 凭证应通过最小权限 ServiceAccount 或专用 Secret 提供,禁止把用户名、token 或私有仓库地址写入策略、PolicyReport、海报和公开日志。
Kyverno 扩展 CEL 还可访问 Kubernetes 资源或受控 HTTP 数据。namespaced 与 cluster-scoped 策略的 HTTP 默认安全行为不同,cluster-scoped 策略的创建权限本身应视为高风险权限。使用 HTTP 前要配置 allowlist/blocklist、TLS、超时、缓存与 fail-open/fail-closed,并验证云 metadata、loopback 和内网管理端点不可达。能在表达式里调用外部接口,不代表应该把高延迟业务 API 放进每次 Deployment 更新的关键路径。
PolicyException 不是永久后门
应用 owner 可以用 NamespacedValidatingPolicy 管理 namespace 内规则,集群级策略则应由平台 owner 管理。PolicyException 可以在不修改原策略的情况下跳过特定对象,但例外必须绑定准确 policy、rule、namespace、对象和期限,并经过与策略同级的审计。宽泛 wildcard、无 owner、无到期时间的例外会把 Deny 变成一份表面严格的许可列表。
管理面至少拆成四类身份:安装/升级 Chart 的平台身份,维护集群级策略和例外的策略身份,reports controller 的只读扫描与报告写入身份,以及 background/cleanup 的主动资源修改身份。不要给四个 controller 共用 cluster-admin。用目标对象反向生成最小 RBAC,再用下面的检查确认实际 ServiceAccount 权限:
kubectl auth can-i --as=system:serviceaccount:kyverno:kyverno-background-controller \
patch deployments.apps -n kyverno-lab
kubectl auth can-i --as=system:serviceaccount:kyverno:kyverno-cleanup-controller \
delete configmaps -n kyverno-lab
kubectl auth can-i --as=system:serviceaccount:kyverno:kyverno-reports-controller \
list deployments.apps -n kyverno-lab结果必须与已启用动作一致:不用 mutate-existing 就不应长期保留 patch 权限,不用 DeletingPolicy 就不应授予广泛 delete。策略表达式、HTTP 返回、镜像 attestation、AdmissionReview 和报告都可能含租户信息;日志与指标应只保留排障字段,对 annotation、镜像名、主体和错误消息做基数与敏感信息治理。
故障要按执行面取第一证据
“策略没生效”至少有六种完全不同的根因。按最终现象直接重装控制器,往往会丢掉最重要的现场。
| 现象 | 先查什么 | 常见根因 | 恢复后的证明 |
|---|---|---|---|
| 策略对象创建失败 | API Status、CRD、kubectl explain | API 版本错误、CEL 类型错误、旧字段机械迁移 | 同一 manifest 可创建且 status 无持续错误 |
| 违规对象被放行 | policy、动态 webhook、namespace/GVK、exception | 未命中、Audit、Ignore、例外过宽 | 同一反例稳定 Deny,正例仍通过 |
| API 写入超时 | webhook failurePolicy、EndpointSlice、证书、controller latency | 无 endpoints、CA 失效、外部 HTTP/Registry 慢 | 故障副本被摘除,尾延迟与错误率回到基线 |
| 新请求有报告,存量没有 | evaluation、backgroundScan、reports leader/RBAC | 策略级或全局扫描关闭、报告控制器失权 | 旧资源进入 BackgroundScanReport 和 PolicyReport |
| generate/mutate-existing 积压 | background leader、UpdateRequest、队列、API throttling | 单 leader、权限不足、自触发循环、写放大 | 队列年龄回落,目标对象与期望收敛 |
| cleanup 未执行或误删 | schedule、selector、Event、delete RBAC | 时区/表达式、匹配过宽、传播策略错误 | 测试对象按计划删除,非目标对象保持不变 |
fail-closed 不能只改一个字段。若 webhook 不可达时选择拒绝,就要同时具备 admission 多副本、跨故障域调度、PDB、稳定 EndpointSlice、证书轮换、API Server 到 webhook 的网络、合理 timeout、Kyverno 管理 namespace 的恢复保护,以及受控、短时、可审计的应急恢复流程。否则一次证书或网络故障会把“安全策略”变成整个集群的写入事故。
管理 namespace 也不能被普通业务策略锁死。恢复路径应限制主体、双人审批、自动到期并保留 audit;恢复后重新运行存量扫描,查明 fail-open 或停机窗口里进入了哪些对象。不要把可复制的永久绕过 annotation、用户名或 namespace 写进公开策略模板。
容量与成本来自请求数、对象数和外部依赖
admission 成本近似由匹配请求速率、每次 CEL/Registry/HTTP 求值成本和重试共同决定;reports 成本由对象数、扫描周期、结果保留类型和报告写放大决定;background/cleanup 成本由 trigger 数、目标数、UpdateRequest 队列和 API Server 限流决定。四条曲线不能合成一个“Kyverno CPU 使用率”。
策略应先用 GVK、operation、namespace/object selector 缩小候选集,再执行 CEL。对报告,可用目标版本支持的 allowed results 减少不需要的 pass/skip 持久化,但不能为了省 etcd 把 error 证据一起抹掉。性能验收应观察请求分位延迟、webhook 错误/超时、controller 队列年龄、API client throttling、报告对象数量与字节、etcd 写入趋势;固定的万能毫秒阈值没有意义,门槛应来自目标 control plane 容量和发布 SLO。
有用的不变量是:扩大策略命中 namespace 时延迟增量可解释;关闭策略后无关 GVK 指标回到基线;background 队列不会随轮次单调增长;报告对象数与被扫描资源和保留结果成比例;Registry 或 HTTP 故障不会造成无限等待;leader 切换后队列继续收敛而不是重复放大写入。
从 legacy 迁移必须一拆多并双轨对照
迁移第一步不是改 YAML,而是冻结行为契约。为每条旧 rule 准备正例、正常违规、字段缺失、UPDATE、background、执行错误、例外和删除生命周期 fixture,记录旧策略的 pass/fail/skip/error、message、patch、generated resource 与 report。随后按动作拆成新对象:
legacy validate 进入 ValidatingPolicy,pattern、foreach 和 deny 改写成 CEL;旧 deny 表示“何时拒绝”,新 validation expression 表示“何时允许”,逻辑必须反转。legacy mutate 进入 MutatingPolicy;strategic merge 要重新选择 ApplyConfiguration 或 JSONPatch,并检查 managedFields 与字段 owner。
legacy generate 进入 GeneratingPolicy,重新验证 generateExisting、synchronize、source 与 downstream 删除行为。legacy verifyImages 进入 ImageValidatingPolicy,重新验收 Registry、信任根、attestation、缓存和故障策略。legacy CleanupPolicy 进入 DeletingPolicy,重新确认 schedule、selector 与 deletion propagation。
allowExistingViolations、failureActionOverrides、manifest validation、Pod Security 快捷字段等旧能力没有简单的一对一字段,可能需要 PolicyException、显式 CEL 或重新设计。kyverno migrate --help 和 迁移指南可以辅助定位映射,但工具生成结果必须经过 fixture,而不是当作语义证明。
双轨时旧策略保持唯一 Deny,新策略先 Audit。比较命中集合、错误原因、patch、生成对象、报告和耗时,差异归零后把新策略切到 Deny,再把旧策略降级为 Audit。经过一个完整发布周期、leader 切换、webhook 故障、background 扫描和回滚演练后,才删除 legacy 对象。两个执行器同时 Deny 会重复计算、增加延迟,并让客户端只看到其中一条错误,不是更稳妥的迁移。
升级 Kyverno 本身也应按版本事务处理:先备份 Helm values、CRD、策略、例外、webhook、报告 schema 与 controller RBAC,在隔离集群用同一 fixture 测试目标 Chart;检查 Kubernetes 支持矩阵、CRD 转换、弃用 flags、动态 webhook diff 和各 controller 指标;生产先灰度 controller,再观察策略结果。回滚必须验证旧版本能读取已经升级的 CRD 和策略对象,不能只保留旧镜像 tag。
清理实验和安全退出
清理顺序要先撤执行点,再删业务 fixture,最后卸载控制器。否则先删 controller 会留下 webhook 指向不存在的 Service,先删 trigger/source/policy 又可能触发生成资源的生命周期动作。
kubectl delete validatingpolicy require-owner-error-lab --ignore-not-found
kubectl delete validatingpolicy require-deployment-owner --ignore-not-found
kubectl delete deployment owner-good owner-bad -n kyverno-lab --ignore-not-found
kubectl delete namespace kyverno-lab
helm uninstall kyverno -n kyverno
kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations \
-l app.kubernetes.io/part-of=kyverno
kubectl get crd | grep -E 'kyverno|policyreport'实验环境可以按 卸载说明处理剩余 CRD;团队环境不能机械删除 CRD,因为策略、例外和报告会随之丢失,finalizer、生成资源和旧 webhook 也可能仍在。长期退出先把所有策略切到观察或移除,确认 API 写入恢复;停止并清空 background/cleanup 工作;归还生成对象的字段所有权;导出脱敏审计证据;撤销 ServiceAccount、Registry 和 HTTP 凭证;再移除 webhook、控制器和 CRD。
退出完成的证据应同时满足:API Server 不再调用 Kyverno webhook,目标请求的允许/拒绝由替代执行器接管,UpdateRequest 和协调队列清空,生成或修改对象都有明确 owner,策略与例外已归档,敏感凭证已撤销,报告与日志按保留策略销毁,资源和费用回到基线。只有这样,Kyverno 才从“还能删掉一套 Helm release”变成一套可安装、可证明、可迁移也可真正退出的策略能力。
