策略还在,Webhook 却消失了
一句话总结
策略文件仍然存在、Webhook 已在 API 服务器中注册,以及该 Webhook 能够响应,是三个不同的事实。在 Webhook 故障实验中,必须分别观察这三项。
为什么需要它
部署负责人说:“安全策略设置为 Fail,但关闭引擎后,违规 Pod 还是创建成功了。”能否立即断定这是 Kubernetes 缺陷?控制器正常关闭时,调用注册可能已经被移除。如果根本没有可调用的 Webhook,就没有真正测试 failurePolicy 如何处理该 Webhook 的调用失败。
2026-09-12,我们也在 LabHub 专用验证 VM 中观察到这种情况。把控制器缩容到 replicas=0 并确认 Pod 消失后,请求仍然通过。当时保存的 ValidatingWebhookConfiguration 列表中没有对应的资源 Webhook。我们没有把它作为“Fail 已失效”的证据。后来,在保留调用注册的情况下,只短暂停止同一个控制器进程的响应,匹配请求在 3 秒后因超时被拒绝。正常关闭与停止响应不是同一种实验。
它如何工作
本单元讨论在 Kyverno v1.19.1 与 k3s v1.35.8+k3s1 上复现的现象。不要将其概括为其他版本或安装选项在正常关闭时也一定会删除 Webhook。迁移实验时,必须重新确认安装版本和实际注册状态。
首先区分三项设置。
- validationActions 的 Deny:验证结果为违规时拒绝请求。
- failurePolicy 的 Fail 与 Ignore:规定无法正常评估策略时如何处理错误。本实验会观察真实 Webhook 调用超时。
- webhookConfiguration.timeoutSeconds:调用等待时限。它并不是等待正常违规响应后再将其改为允许的计时器。
Ignore 并不表示“不验证”。如果服务器正常判断出违规并返回拒绝响应,该拒绝仍然有效。因此,进行 Ignore 实验时,也必须先向正常服务器发送违规请求,确认它会被拒绝。没有这条基线,只看到故障期间请求成功,就无法区分策略是否从一开始便未生效。
Kyverno ValidatingPolicy 官方说明介绍了策略字段和自动生成行为。该 API 为 policies.kyverno.io/v1。不要原样搬用旧式 ClusterPolicy 示例中的字段位置,应对照已安装的 CRD 与 kubectl explain。通过 Kubernetes 自身 ValidatingAdmissionPolicy 执行的路径也彼此独立,不要把它混入引擎进程停止实验。
命名空间就是实验范围
在目标命名空间中应用一条要求 Pod 具有 environment 标签的创建规则,对照命名空间则放在选择范围之外。namespaceSelector 选择的是命名空间标签,而不是 Pod 标签。Pod 上的 environment 与用于选择命名空间的 kubernetes.io/metadata.name 属于不同层次。
只阅读策略声明并不够。还要在生成的 Webhook 中检查真实 service 路径、failurePolicy、timeoutSeconds、namespaceSelector 和 CREATE pods 规则。如果存在额外 objectSelector 或 matchConditions,请求可能绕过验证,因此也要一并检查。范围外的正常行为是对照组,而不是范围内未发生故障的证据。
Kubernetes 动态准入官方文档区分了请求匹配与 Webhook 调用失败处理。准确说法不是“所有创建请求都会被阻止”,而是“与该 Webhook 匹配的请求会因调用失败而被阻止”。
两个相同请求与一个对照组
每次实验都要使用新的 Pod 名称。复用已有名称会产生 AlreadyExists,并与策略拒绝或超时混淆。除了 API 响应,还要查询并记录对象是否真实保存以及 UID。不要仅凭一行成功消息推断对象已保存。
| 条件 | 目标范围内带正常标签的请求 | 目标范围内的违规请求 | 范围外的违规请求 |
|---|---|---|---|
| Webhook 正常,Deny | 保存 | 明确的策略拒绝 | 单独检查范围 |
| 保留注册、停止响应,Fail | 超时且未保存 | 超时且未保存 | 保存 |
| 保留注册、停止响应,Ignore | 等待时限后保存 | 等待时限后保存 | 保存 |
| Webhook 恢复 | 单独确认现有 Pod | 再次明确拒绝 | 按恢复范围检查 |
表格中的两个故障行是在上述专用 VM 中观察到的结果。我们为每次请求保存了原始错误。Fail 错误来自调用相应 Webhook 时发生 context deadline exceeded;正常违规错误则由相应策略返回 denied the request。两者在终端里都像失败,但原因不同。DNS 错误或 connection refused 又属于其他故障类型,不能用本次仅停止响应的实验成功来代替验证。
已运行的 Pod 为何会留下
admission 是请求写入存储前的关口。本次规则针对 CREATE pods,并关闭 background 评估。我们并没有创建一个会自动删除之前已允许并正在运行的 Pod 的控制器。因此,应单独观察现有 Pod 的 UID 保持不变且状态仍为 Running。不能把以同名重新创建的 Pod 算作“存活”。
实际现场中的表现
假设星期五发布前,安全引擎响应开始变慢。切换为 Ignore 可能改善部署可用性,却会引入接受未经评估变更的风险。保持 Fail 可能导致相应请求停止,却能关闭未经验证便写入存储的路径。无论选择哪一项,都必须先确定目的、范围和可容忍的故障时长。这不能用“大多数时候 Ignore 更好”一句话决定。
Kubernetes Webhook 设计指南中的 fail-open 建议,是在同时考虑变更 Webhook 与最终状态验证的语境下提出的。不要把它理解成对所有安全验证 Webhook 无条件使用 Ignore 的建议。还必须同时设计高可用、缩小调用范围、充足资源与故障告警。
replicas 恢复与策略恢复并非同一时刻
把控制器副本数恢复为 1,并不表示验证会立刻恢复。应分别确认新 Pod 启动、领导者接管、Webhook 注册和真实策略响应。本单元第一次学生路径测试中,replicas 在 23:49:53 UTC 恢复,但新进程直到 23:50:27 才获得 leader Lease。20 秒的注册等待在此之前就已结束。这不是 Fail 对策略违规的判定,而是实验运行器的就绪等待失败。
Kyverno v1.19.1 的领导者选举实现使用 Lease,并配置为退出时不立即释放。实际等待时间随安装方式和负载变化。不要把观察到的 34 秒视为所有环境的常量,应检查日志与注册状态。即使恢复期间等待失败,实验仍会保留缩容期间观察到的策略、注册与请求结果。点击重试按钮前,应先区分哪些内容已经恢复、哪些仍未得到确认。
制造故障的工具也需要恢复设计
暂停只应用于学生专用 VM 内的单个 Kyverno 容器。实验会检查容器 ID、Pod UID 与 PID 启动时间,并通过 Linux pidfd 绑定信号,避免因数字 PID 被复用而影响其他进程。独立监视器会在 20 秒内自动恢复进程,且不会把暂停与恢复拆分到两个步骤按钮之间。即使实验运行器被强制终止,或在返回就绪响应前消失,监视器仍会尝试恢复目标。
这套装置不能替代正常关闭流程、高可用测试或生产故障演练。连监视器本身也被终止,或 VM 被关闭,属于其他故障域。评分器不会重新制造故障,只会读取已保存的结果。基于文件的评分也不是能够阻止恶意 root 伪造全部证据的安全认证;它用于发现学习过程中观测依据缺失或彼此矛盾的情况。
下一项实验将做什么
第一项实验处理正常缩容与注册缺失。我们会记录目标范围和对照范围,亲自编写 ValidatingPolicy,然后观察正常基线及正常缩容时的注册状态。还会区分恢复请求、确认注册与真实拒绝响应的时间,并通过服务器 dry-run 验证“通过验证”和“写入存储”是不同阶段。
第二项实验在新的 VM 中独立开始,不需要第一项实验的文件。我们会重新建立策略和正常基线,再通过保留调用注册的短暂响应中断比较 Fail 与 Ignore。两项实验都会分别记录策略存在、注册、响应和现有 Pod 存活情况。必须先有原始材料,再得出结论。
还要确认 dry-run 在哪里执行
根据 Kubernetes 的服务器 dry-run 官方说明,服务器 dry-run 会在不保存变更的情况下执行该请求的验证路径。相比之下,客户端 dry-run 在本地构造输出,因此无法证明服务器 Webhook 是否会拒绝请求。检查文件语法、服务器接受请求,以及对象真实写入存储,是三种不同的验证。
本实验会使用唯一名称,分别对正常请求和违规请求执行 server dry-run,并单独查询同名 Pod。正常响应中的 kind、名称、namespace 与标签必须正确,同时不得存在保存结果。违规请求必须是带有策略标识的明确拒绝。不要因为成功输出中出现 Pod 对象就记录为已保存,也不要把 AlreadyExists 记录成策略拒绝。接收请求的 Webhook 也必须支持 dry-run 和副作用处理,因此迁移到其他安装环境时,还应检查真实 Webhook 的 sideEffects 设置。