
`kubectl auth can-i create pods` 显示 yes，创建 Pod 却被安全策略拒绝。另一次部署没有修改权限配置，只因 Webhook 服务停机就无法创建资源。两种现象都发生在认证、授权之后：准入还要检查这次操作携带的对象，以及集群要求它遵守的约束。

准入是 API 写入链路的一部分。把 Webhook 当成一段独立的校验代码，会漏掉匹配范围、修改顺序和故障策略；把它当成所有请求必经的网关，又会高估它的影响范围。本文固定 Kubernetes v1.35.1，沿准入实现和 TLS Webhook 实验解释这些条件。

<!--more-->

## RBAC 允许动作，不代表对象满足准入要求

认证回答请求者是谁，授权回答这个身份是否可以执行该动作。准入进一步检查经过授权的对象操作，例如 Pod 是否允许使用特权容器、Namespace 的配额是否足够、某个字段是否需要默认值。

准入主要作用于资源创建、更新、删除、连接这类请求；读取对象和列举对象不经过通常的资源准入链。具体 Webhook 还要按配置的 operations 和资源匹配。一个只监听 `CREATE deployments` 的 Webhook，不会因为它处理了 Deployment 就自动收到随后创建的每一个 Pod；Pod 是控制器发起的另一批 API 请求。

通过 mutating admission 的对象可能已不同于客户端提交的内容。后续 validating admission 检查修改后的对象，之后才允许目标对象进入持久化。单个 Webhook 返回 allowed，并不等于整个请求成功，后面的插件、校验或存储操作仍可能失败。

```mermaid
sequenceDiagram
    participant C as 客户端
    participant A as API Server
    participant M as Mutating Webhook
    participant V as Validating Webhook
    participant E as 存储
    C->>A: 带对象的 API 请求
    A->>A: 认证和授权
    A->>M: AdmissionReview
    M-->>A: allowed 和可选 JSONPatch
    A->>A: 将补丁应用到对象
    A->>V: 包含修改后对象的 AdmissionReview
    V-->>A: allowed 或拒绝原因
    A->>E: 全部检查通过后写入目标对象
    E-->>A: 写入结果
    A-->>C: API 响应
```

这是修改后通过校验的一条请求路径，省略内置插件和多 Webhook 情形。Webhook 响应中的 UID 对应它收到的 AdmissionReview 请求，不是 Pod UID；不同 Webhook 调用也不应依靠相同 AdmissionReview UID 关联同一次外部请求。

## 修改需要顺序，校验可以并行

[mutatingDispatcher.Dispatch](https://github.com/kubernetes/kubernetes/blob/v1.35.1/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go#L164-L195) 按匹配结果调用修改 Webhook，并把变更应用到本次请求的对象。后续修改器看到的内容可能已经包含前面的补丁。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/mutating/dispatcher.go#L164-L195" >}}

这并不意味着应该让多个修改器依赖一个脆弱的名字排序。配置、匹配条件或其他插件变化，都可能改变参与修改的集合。启用合适的 `reinvocationPolicy` 后，部分修改器还可能被再次调用。因此，修改器应能处理字段已经存在的对象，并避免重复添加同一 sidecar、重复附加列表项。需要最终保证的条件，应由 validating 机制检查。

[validatingDispatcher.Dispatch](https://github.com/kubernetes/kubernetes/blob/v1.35.1/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go#L126-L165) 收集匹配的 Webhook，随后启动 goroutine 并发调用，并等待结果汇总。校验器不修改对象，不应依赖另一个校验器先运行，也不能从本次日志顺序推出跨 Webhook 的普遍先后关系。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go#L126-L165" >}}

准入不是外部副作用的事务。如果 Webhook 已创建一个外部资源，随后另一个插件拒绝请求，API Server 不会替它撤销那个外部资源。声明 `sideEffects: None` 必须符合实际行为，不能只为让 dry-run 请求通过而填写。必须产生外部副作用的业务，通常更适合持久化对象后由控制器以幂等方式推进。

## 匹配条件决定故障影响面

WebhookConfiguration 的 rules 决定 operation、API group、版本、资源和子资源；`namespaceSelector`、`objectSelector` 和匹配条件还能进一步收窄范围。Webhook 自己停机时，只有匹配并实际需要调用它的请求，才进入它的故障策略。

Namespace 标签策略应由受信任的管理者维护。如果低权限用户能够自行移除策略依赖的标签，就不能把标签匹配当成不可绕过的安全边界。`objectSelector` 对象标签同理，适合有明确管理边界的选择场景。

实际调用前的匹配入口是 dispatcher 使用的 `ShouldCallHook`。它返回没有 invocation 时，dispatcher 直接跳过调用。这比单看 Webhook 服务的可用性更能解释“为什么同一时刻有些资源能创建，有些不能”。[官方动态准入文档](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)列出了具体匹配字段和适用语义。

## Fail 和 Ignore 处理的是调用失败

未显式填写不表示没有策略。v1 的默认处理给 mutating 与 validating Webhook 设置 `failurePolicy: Fail`、`timeoutSeconds: 10`，mutating Webhook 的 `reinvocationPolicy` 默认为 `Never`。允许的 timeout 范围是 1–30 秒；它约束一次调用，不是整条请求的所有 Webhook 加起来只能用这么久。复核故障行为时应查看 API 中默认处理后的配置，而不是把省略字段理解成 Ignore 或无限等待。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/apis/admissionregistration/v1/defaults.go#L31-L80" >}}

超时取值边界由 API 校验执行，不是 Webhook 服务自行约定。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/pkg/apis/admissionregistration/validation/validation.go#L384-L390" >}}

`timeoutSeconds` 限制一次 Webhook 调用等待时间。TLS 失败、连接失败、超时或无法得到有效响应时，`failurePolicy: Fail` 会让匹配请求失败；`Ignore` 允许跳过这次调用错误，继续其他检查。Ignore 不保证最终创建成功，其他准入条件仍可能拒绝。

更容易误解的是显式拒绝。Webhook 正常返回 `allowed: false`，表示策略已经做出了拒绝判断，不属于“调用失败”。[validating dispatcher](https://github.com/kubernetes/kubernetes/blob/v1.35.1/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go#L170-L218) 区分 `ErrCallingWebhook` 与 `ErrWebhookRejection`：只有前一类进入 Ignore 的放行分支，显式拒绝仍作为错误返回。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/staging/src/k8s.io/apiserver/pkg/admission/plugin/webhook/validating/dispatcher.go#L170-L218" >}}

Fail 提高约束在故障时的强制性，也让 Webhook 的可用性进入 API 写入可用性链路。Ignore 降低这项故障依赖，同时意味着故障窗口内可能保存未经该 Webhook 检查的对象。选择哪个策略，要根据这条约束失效的后果判断；不能把 Ignore 统一当成生产环境的保险开关。

超时也不等于撤销 Webhook 已执行的动作。API Server 停止等待后，服务端处理器和它调用的外部系统仍可能继续工作。准入逻辑应尽量保持纯检查或纯对象变换，不把超时重试变成重复副作用。

## 实验：修改、拒绝、超时与服务停机

实验在 `zj` 的独立 kind 集群 `article-api` 执行，kind v0.33.0，客户端和服务端 Kubernetes v1.35.1，提交 `8fea90b45245ef5c8ba54e7ae044d3e777c22500`。配置、脚本、证据和复现说明见 [k8sdev/article-api-admission](https://github.com/jimyag/k8sdev/tree/3cfa654905a750b1716de8c32aa42d7573220954/article-api-admission)。先按该目录 README 的固定提交准备步骤创建独立工作目录，再运行实验。

Webhook 实验分别验证显式拒绝和超时，脚本按实际请求结果等待配置生效。

{{< github-file url="https://github.com/jimyag/k8sdev/blob/3cfa654905a750b1716de8c32aa42d7573220954/article-api-admission/run.py#L59-L85" >}}

测试服务运行在专用 Docker 容器，使用临时自签名证书。它共享本实验 kind 节点的网络，以固定 TLS 端口供 API Server 访问；不修改宿主机防火墙，也不把私钥保存在实验结果中。服务只匹配带专用标签的 Namespace 内 ConfigMap CREATE，避免把故障注入扩散到其他资源。

修改器给对象加 `article.example/mutated=yes` annotation。创建后重新读取 ConfigMap，实际存在该值。校验器在 `data.deny=true` 时返回拒绝，实际响应包含：

```text
admission webhook "validate.article.example" denied the request:
article policy denied this ConfigMap
```

校验器的 failurePolicy 设置为 Ignore 后，显式拒绝仍被返回。这里测试的是策略拒绝与调用故障的区别，不能把这个错误解释成 Webhook 不健康。

服务对 `timeout-` 前缀对象模拟处理耗时超过调用的 `timeoutSeconds=1`。Ignore 策略下，这次超时后的创建成功；确认这项结果后再发起上述显式拒绝请求，仍然被拒绝。Fail 策略下，超时返回 `context deadline exceeded`。随后停止服务容器，Fail 策略下得到 `connect: connection refused`。这两个错误代表不同的调用失败，但都让 Fail 策略下的匹配创建失败。

同一停机窗口，在未带匹配标签的 `default` Namespace 创建 ConfigMap 成功。把校验器策略改成 Ignore 后，在实验 Namespace 的创建也成功。脚本观察实际创建结果，等待配置变更生效，未用固定等待时间假定所有 API Server 缓存已经更新。

实验逐项保存成功对象、拒绝消息与 Webhook 的对象名、AdmissionReview UID；没有记录 token 或完整敏感对象。它证明了这组匹配配置下的行为，不代表测试了所有资源、所有操作和所有内置插件顺序。

## Pod Security Admission 是另一种内置约束

阻止特权 Pod 不一定需要维护自定义 Webhook。Pod Security Admission 根据 Namespace 的策略标签，检查 Pod 是否符合 `privileged`、`baseline` 或 `restricted` 标准。`enforce` 拒绝不符合要求的 Pod，`audit` 和 `warn` 分别提供审计与客户端警告；它们可以配置不同级别和版本。

[ValidatePod 与 EvaluatePod](https://github.com/kubernetes/kubernetes/blob/v1.35.1/staging/src/k8s.io/pod-security-admission/admission/admission.go#L455-L491) 读取有效 Namespace 策略，按级别和版本运行检查。Pod 控制器模板可以获得警告与审计信息，但 enforce 的关键检查落在真实 Pod 请求上，因此 Deployment 对象创建成功，不保证控制器随后创建的 Pod 也能通过。

{{< github-file url="https://github.com/kubernetes/kubernetes/blob/v1.35.1/staging/src/k8s.io/pod-security-admission/admission/admission.go#L455-L491" >}}

实验把独立 Namespace 设置为 `enforce=restricted`、`enforce-version=v1.35`。提交特权 Pod 后返回 Forbidden，列出 privileged、allowPrivilegeEscalation、capabilities、runAsNonRoot 和 seccompProfile 的不符合项。随后提交显式满足这些条件的 Pod，API 创建成功。这里验证的是准入允许，不把它扩大为镜像已经正常启动或应用一定安全。

restricted 不会替用户把 Pod 自动修改成安全配置，也不覆盖所有业务安全要求。需要按租户限制镜像来源、业务标签或组织规则时，再选择 ValidatingAdmissionPolicy 或 Webhook；需要注入字段时才考虑 mutating 机制。已有的 [Webhook 自签名证书文章](/posts/kubernetes-webhook-admission-certificate-configuration/)讨论 TLS 建链，证书可信和策略判断正确仍是两个独立条件。

