Kubernetes 基础体系 · 第 49/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。

Kubernetes Admission Control:内置插件、Webhook、策略、失败和可用性

Kubernetes API 请求通常经历以下阶段:

flowchart LR
    C[客户端] --> AN[认证 Authentication]
    AN --> AZ[授权 Authorization]
    AZ --> AD[Admission Control]
    AD --> V[对象默认值与变更]
    V --> P[策略校验]
    P --> S[持久化到 etcd]
    S --> R[控制器、调度器、kubelet观察对象]

Admission Control,即准入控制,是 API Server 在请求通过认证和授权后、对象写入存储前执行的一组检查与变更机制。它可以:

  • 修改即将写入的对象,例如补充默认字段;
  • 拒绝不符合规则的请求;
  • 根据命名空间、用户、资源、操作等条件选择性生效;
  • 在对象以 dryRun 方式提交时参与检查;
  • 为策略执行提供统一入口。

准入控制不等于认证,也不等于授权:

  • 认证 Authentication 回答“请求者是谁”;
  • 授权 Authorization 回答“请求者是否有权执行这个动作”;
  • 准入控制 Admission 回答“即使有权执行,这个具体对象是否允许进入集群,以及是否需要被修改”。

因此,一个请求必须同时满足:

最终接受=认证成功授权允许所有适用的准入检查通过\text{最终接受} = \text{认证成功} \land \text{授权允许} \land \text{所有适用的准入检查通过}

其中“所有适用的准入检查”包括内置插件、策略型准入控制器和外部 Webhook。


一、准入控制处理的是什么对象

准入控制处理的是发送到 Kubernetes API Server 的 API 请求,而不是集群中所有状态变化。

例如:

kubectl create -f pod.yaml
kubectl patch deployment web -p '{"spec":{"replicas":3}}'
kubectl delete pod web-abc

这些请求都可能经过准入控制。

但是,Admission 不会周期性扫描已有对象,也不会自动修复过去已经写入的对象。假设某个新的安全策略今天启用:

  1. 昨天已经存在的 Pod 不会因为策略启用而自动被删除;
  2. 今天新创建的 Pod 会被检查;
  3. 如果 Deployment 因为模板变化而触发新的 Pod 创建,新 Pod 会被检查;
  4. 对已有对象执行后续 UPDATE 时,该 UPDATE 请求可能被检查。

这一区别十分重要。准入策略通常是写入时控制,不是持续合规扫描系统。

Admission 也不直接保证运行时安全。例如,一个 Pod 通过了准入检查,之后容器进程仍可能因为漏洞、配置错误或节点问题产生风险。准入控制解决的是“什么对象可以进入 API 对象状态”,不是完整的运行时防护。


二、完整请求路径:认证、授权和准入的先后关系

以创建一个 Pod 为例,请求路径可以简化为:

sequenceDiagram
    participant K as kubectl
    participant A as kube-apiserver
    participant AU as Authentication
    participant Z as Authorization
    participant M as Mutating Admission
    participant W1 as Mutating Webhook
    participant V as Validating Admission
    participant W2 as Validating Webhook
    participant E as etcd

    K->>A: POST /api/v1/namespaces/dev/pods
    A->>AU: 认证请求
    AU-->>A: user=alice, groups=...
    A->>Z: 授权 create pods in dev
    Z-->>A: allow
    A->>M: 内置变更插件
    A->>W1: AdmissionReview
    W1-->>A: patch / allow
    A->>V: 内置校验插件
    A->>W2: AdmissionReview
    W2-->>A: allowed=true
    A->>E: 持久化最终对象
    A-->>K: 201 Created

如果授权失败,请求不会进入准入阶段。反过来,即使授权成功,准入插件也可以拒绝请求。

例如:

alice 被 RBAC 允许在 dev 命名空间创建 Pod
但 Pod 使用了 restricted Pod Security Standard 不允许的能力
结果:授权成功,准入拒绝

准入控制器还可以读取认证阶段产生的用户信息。Webhook 的 AdmissionReview.request.userInfo 中通常包含用户名、用户组和额外信息。Webhook 不应通过客户端自定义 HTTP Header 判断调用者身份,因为这些 Header 可能由不可信客户端构造;应使用 API Server 传入的 AdmissionReview 用户信息。


三、准入控制的两个核心阶段:变更和校验

准入控制通常分为两个逻辑阶段。

1. Mutating:变更阶段

变更准入控制器可以修改对象,例如:

  • 为容器注入 sidecar;
  • 添加默认的资源请求;
  • 注入标签、注解;
  • 设置安全相关字段;
  • 根据命名空间策略补充配置。

变更之后,后续校验看到的是变更后的对象

假设客户端提交:

apiVersion: v1
kind: Pod
metadata:
  name: demo
spec:
  containers:
  - name: app
    image: nginx:1.27

一个 Mutating Webhook 添加:

spec:
  securityContext:
    runAsNonRoot: true

那么后续 Validating Webhook 检查的应是包含该字段的最终候选对象,而不是客户端最初提交的版本。

2. Validating:校验阶段

校验准入控制器不能修改对象,只能返回允许或拒绝。例如:

  • 镜像必须来自指定仓库;
  • Pod 必须设置资源请求;
  • Service 不得使用指定端口;
  • 生产命名空间不允许 privileged
  • 一个租户不能超过资源配额。

可以把一次请求抽象为:

O0MutatingO1Validating{allow,deny}O_0 \xrightarrow{\text{Mutating}} O_1 \xrightarrow{\text{Validating}} \{\text{allow},\text{deny}\}

  • O0O_0:客户端提交的对象;
  • O1O_1:所有变更完成后的候选对象;
  • allow:对象可以继续写入;
  • deny:API Server 返回错误,通常是 HTTP 4xx。

如果校验逻辑依赖某个字段,而该字段可能由变更插件补充,就必须确保校验发生在变更之后。Webhooks 不应依赖一个脆弱的固定顺序来“碰巧”满足这个条件,而应设计为能够处理最终对象。


四、内置 Admission 插件

内置插件是编译在 API Server 中或由 API Server 直接加载的准入控制逻辑,不需要额外部署网络服务。它们通常延迟更低、故障面更小,但具体默认启用集合会随 Kubernetes 版本和发行版变化。

可以使用 API Server 参数查看配置,例如:

ps -ef | grep kube-apiserver

在 kubeadm 集群中,也可以查看静态 Pod 清单:

grep -E -- '--enable-admission-plugins|--disable-admission-plugins' \
  /etc/kubernetes/manifests/kube-apiserver.yaml

这些命令需要节点管理员权限,并且只适用于相应部署方式。托管 Kubernetes 中,控制面参数通常由云厂商管理,不能直接修改。

1. 常见内置插件及其作用

NamespaceLifecycle

防止在已终止或不存在的命名空间中创建对象,并处理命名空间生命周期相关约束。

它解决的是 API 对象生命周期问题,不是 RBAC 授权。一个用户即使拥有创建 Pod 的 RBAC 权限,也不能在一个不存在的命名空间中成功创建 Pod。

LimitRanger

根据命名空间中的 LimitRange 为容器补充默认资源请求和限制,或拒绝不符合范围的资源配置。

例如:

apiVersion: v1
kind: LimitRange
metadata:
  name: container-limits
  namespace: dev
spec:
  limits:
  - type: Container
    defaultRequest:
      cpu: 100m
      memory: 128Mi
    default:
      cpu: "1"
      memory: 512Mi

如果 Pod 容器没有指定资源字段,准入阶段可能得到默认值:

resources:
  requests:
    cpu: 100m
    memory: 128Mi
  limits:
    cpu: "1"
    memory: 512Mi

LimitRange 影响的是单个对象或容器的资源约束;它不负责限制整个命名空间的总量。

ResourceQuota

根据命名空间中的 ResourceQuota 限制总资源使用量和对象数量。

apiVersion: v1
kind: ResourceQuota
metadata:
  name: team-quota
  namespace: dev
spec:
  hard:
    requests.cpu: "4"
    requests.memory: 8Gi
    limits.cpu: "8"
    limits.memory: 16Gi
    pods: "20"

当新对象会使配额超限时,请求被拒绝。典型错误类似:

exceeded quota: team-quota, requested: pods=1,
used: pods=20, limited: pods=20

ResourceQuota 通常要求对象位于命名空间中。它不等于调度器资源判断:配额检查关注命名空间总量,调度器关注具体节点是否能放置 Pod。

ServiceAccount

为 Pod 补充默认 ServiceAccount,或者执行与 ServiceAccount 相关的准入行为。

如果 Pod 没有显式指定:

spec:
  serviceAccountName: build

它通常会使用命名空间中的 default ServiceAccount。ServiceAccount 的身份不会自动赋予业务权限;Pod 能访问哪些 API,仍由 RBAC 等授权机制决定。

DefaultStorageClass

当 PVC 没有指定 storageClassName 时,可以根据默认 StorageClass 补充存储类。

它依赖集群中存在被标记为默认的 StorageClass。云厂商可能预置不同的默认存储类,因此同一份 PVC YAML 在不同集群中的最终对象可能不同。

DefaultTolerationSeconds

为 Pod 的某些节点故障相关 Taint 补充默认容忍时间。它影响 Pod 在节点异常场景下的驱逐行为,但不等于高可用保证。

PodSecurity

PodSecurity 实现 Pod Security Admission(PSA),根据命名空间标签执行 Pod Security Standards。它是内置安全准入控制器,不需要单独运行一个 Webhook。

典型命名空间标签:

kubectl label namespace dev \
  pod-security.kubernetes.io/enforce=baseline \
  pod-security.kubernetes.io/enforce-version=latest \
  pod-security.kubernetes.io/audit=restricted \
  pod-security.kubernetes.io/audit-version=latest \
  pod-security.kubernetes.io/warn=restricted \
  pod-security.kubernetes.io/warn-version=latest

三个模式的含义不同:

  • enforce:违反策略时拒绝请求;
  • audit:允许请求,但把违规信息写入审计事件;
  • warn:允许请求,但向客户端返回警告。

例如,一个 Pod 可能因为使用特权容器而在 baseline 下被拒绝。restricted 还会要求更严格的安全上下文,例如非 root、禁止不安全能力、使用受约束的卷类型等。具体规则由所选 Kubernetes 版本的 Pod Security Standards 定义,不能把某一版本的字段清单永远视为不变。

一个常见迁移过程是先使用:

warn=restricted
audit=restricted
enforce=baseline

观察工作负载实际违规情况,再决定是否提升 enforce。这不是绕过策略,而是将“发现问题”和“阻止发布”分离。


五、启用、禁用和默认行为

自建控制面通常通过 API Server 参数配置插件:

--enable-admission-plugins=NodeRestriction,PodSecurity,ResourceQuota
--disable-admission-plugins=SomePlugin

实际参数必须以运行中的 Kubernetes 版本文档和发行版默认配置为准。不能因为某个插件在一套集群中默认启用,就推断所有集群都相同。

修改 API Server 的准入插件通常会导致控制面组件重启或滚动更新,因此要考虑:

  1. 新旧 API Server 实例是否配置一致;
  2. 负载均衡器是否会把请求发送到不同配置的实例;
  3. 某些请求是否在升级期间表现不一致;
  4. 关闭 ResourceQuotaPodSecurity 等插件是否造成安全或资源边界突然失效。

内置插件的一个重要优势是它们通常不需要经过集群 Service、DNS、网络策略和 TLS 证书路径,因此在控制面压力或数据面异常时,故障面比外部 Webhook 小。


六、Admission Webhook:把准入逻辑放到 API Server 外部

Admission Webhook 是 API Server 调用的 HTTPS 服务。它通过 AdmissionReview 接收请求,并返回允许、拒绝或变更结果。

Webhook 分为两类:

  • MutatingAdmissionWebhook:允许返回 JSON Patch 修改对象;
  • ValidatingAdmissionWebhook:只能返回是否允许,不能修改对象。

Webhook 常用于内置插件无法表达的业务规则,例如:

  • 镜像必须经过企业镜像扫描;
  • 生产环境必须带有成本中心标签;
  • 不同租户不能使用相同的外部域名;
  • 自动注入代理、证书或配置;
  • 根据自定义资源状态校验普通 Kubernetes 对象。

但 Webhook 也把 API Server 的请求路径变成了网络调用,因此它会引入超时、证书、DNS、Service、Endpoint、网络策略和自身依赖等故障因素。


七、Webhook 的 API 配置

一个简化的 Validating Webhook 配置如下:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: image-policy.example.com
webhooks:
- name: image-policy.example.com
  admissionReviewVersions:
  - v1
  sideEffects: None
  failurePolicy: Fail
  timeoutSeconds: 5
  matchPolicy: Equivalent
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - pods
    scope: Namespaced
  namespaceSelector:
    matchLabels:
      admission.example.com/enabled: "true"
  clientConfig:
    service:
      namespace: admission-system
      name: image-policy
      path: /validate
      port: 443
    caBundle: <base64-encoded-CA-certificate>

这个配置表达了以下条件:

  1. 只匹配核心 API 组 "" 中的 v1 Pod;
  2. 只处理 CREATEUPDATE
  3. 只处理命名空间级资源;
  4. 只处理带有指定命名空间标签的命名空间;
  5. API Server 通过 admission-system/image-policy Service 的 443 端口调用 Webhook;
  6. API Server 使用 caBundle 验证 Webhook 服务端证书;
  7. Webhook 最多等待 5 秒;
  8. Webhook 调用失败时拒绝请求;
  9. sideEffects: None 表示 Webhook 不会产生请求之外的外部副作用;
  10. matchPolicy: Equivalent 允许 API Server 将等价 API 版本映射到该规则。

caBundle 不是 Webhook 服务端证书本身,而是用于验证该证书的 CA 证书。服务端证书的 DNS 名称必须与 Service 访问名称匹配,通常需要包含:

image-policy.admission-system.svc
image-policy.admission-system.svc.cluster.local

实际证书管理可以使用 cert-manager、控制面集成方案或自建轮换系统,但证书轮换必须同时保证:

  • Webhook 服务加载了新证书;
  • API Server 配置中的 caBundle 已更新;
  • 旧证书在过渡期仍然可验证,或者切换顺序不会造成中断;
  • 多副本 Webhook 使用一致的信任配置。

matchPolicy

matchPolicy 常见取值是:

  • Exact:只匹配规则中列出的 API 版本;
  • Equivalent:匹配 Kubernetes 认为等价的 API 版本,再向 Webhook 发送请求。

对常用内置资源使用 Equivalent 通常更能避免客户端使用不同 API 版本时绕过规则。但 Webhook 必须正确解析实际收到的 AdmissionReview 和对象版本。

选择器

Webhook 可以通过以下条件缩小范围:

  • namespaceSelector:根据命名空间标签匹配;
  • objectSelector:根据对象标签匹配;
  • rules:根据 API 组、版本、资源、操作和 scope 匹配;
  • 某些版本还支持基于 CEL 的 matchConditions

objectSelector 特别容易被误用:如果允许用户自行添加或删除标签,就可能通过改变标签绕过 Webhook。它适合做性能优化或明确可信的对象分流,不应单独承担不可绕过的安全边界。


八、AdmissionReview:Webhook 实际收到什么

API Server 发送的请求体类似:

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "request": {
    "uid": "5d2b...",
    "kind": {
      "group": "",
      "version": "v1",
      "kind": "Pod"
    },
    "resource": {
      "group": "",
      "version": "v1",
      "resource": "pods"
    },
    "requestKind": {
      "group": "",
      "version": "v1",
      "kind": "Pod"
    },
    "requestResource": {
      "group": "",
      "version": "v1",
      "resource": "pods"
    },
    "name": "demo",
    "namespace": "dev",
    "operation": "CREATE",
    "userInfo": {
      "username": "alice",
      "groups": ["developers"]
    },
    "object": {
      "apiVersion": "v1",
      "kind": "Pod",
      "metadata": {
        "name": "demo"
      }
    },
    "oldObject": null,
    "dryRun": false,
    "options": {}
  }
}

处理时必须注意:

  • CREATE 通常只有 object
  • UPDATE 同时有 oldObject 和新 object
  • DELETEobject 是被删除对象,具体行为要结合资源和 API Server 请求语义处理;
  • dryRun: true 表示请求不能产生持久化副作用;
  • subResource 可能出现在子资源请求中,例如 pods/status
  • uid 必须原样放回响应,用于将响应关联到原始请求;
  • AdmissionReviewapiVersion 必须使用双方协商支持的版本。

一个 Validating Webhook 的成功响应:

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "5d2b...",
    "allowed": true
  }
}

拒绝响应:

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "5d2b...",
    "allowed": false,
    "status": {
      "code": 403,
      "reason": "image registry is not allowed",
      "message": "container app uses registry docker.io"
    }
  }
}

拒绝原因应说明资源、字段和修复方向,但不应把凭据、内部密钥或不必要的敏感对象内容写进错误消息。

Mutating Webhook 可以返回 JSON Patch。例如,把:

{
  "metadata": {
    "name": "demo"
  }
}

变更为添加标签:

[
  {
    "op": "add",
    "path": "/metadata/labels",
    "value": {
      "policy.example.com/checked": "true"
    }
  }
]

返回时还需要声明:

"patchType": "JSONPatch",
"patch": "W3sib3AiOiJhZGQiLCJwYXRoIjoiL21ldGFkYXRhL2xhYmVscyIsInZhbHVlIjp7InBvbGljeS5leGFtcGxlLmNvbS9jaGVja2VkIjoidHJ1ZSJ9fV0="

真实实现必须正确处理 JSON Pointer 转义:

  • / 要编码为 ~1
  • ~ 要编码为 ~0

因此,直接字符串拼接 Patch 路径是常见错误。


九、变更 Webhook 的顺序、重复调用和幂等性

多个 Mutating Webhook 不是并行随意执行的。API Server 需要按顺序应用变更,因为后一个 Webhook 可能看到前一个 Webhook 修改后的对象。

同时,变更 Webhook 可能因为其他 Webhook 的后续修改而被重新调用。reinvocationPolicy 控制这种重新调用行为:

  • Never:不重新调用;
  • IfNeeded:如果后续变更可能使当前 Webhook 的变更不再满足,就允许重新调用。

因此,Mutating Webhook 必须满足幂等性:

M(M(O))=M(O)M(M(O)) = M(O)

其中 MM 是变更函数,OO 是对象。

例如,下面的逻辑不是幂等的:

每次调用都追加一个 sidecar

第一次得到:

app, proxy

第二次得到:

app, proxy, proxy

正确做法是先判断容器名、注解或哈希标记是否已经存在:

如果不存在 proxy 容器,则添加;
如果已经存在且配置相同,则不变;
如果已经存在但配置不兼容,则返回明确错误或执行可证明安全的更新。

变更 Webhook 还应避免覆盖用户明确设置的字段。一个默认注入器应区分:

字段不存在:可以提供默认值
字段已存在:除非策略明确要求覆盖,否则保留用户值

否则,多个 Webhook 之间会出现“互相覆盖”,导致最终对象依赖不可见的调用顺序。


十、Validating Webhook 的并发和一致性

Validating Webhook 只返回允许或拒绝,因此多个校验通常可以并行执行。最终结果要求所有适用校验都通过:

allowed=i=1nallowedi\text{allowed} = \bigwedge_{i=1}^{n} \text{allowed}_i

只要任意一个适用的校验返回拒绝,整个 API 请求就失败。

这带来两个工程后果:

  1. 校验服务应尽量使用只读、低延迟逻辑;
  2. 不同校验之间不能假设某个 Webhook 一定先完成。

例如,Webhook A 给对象添加标签,Webhook B 检查该标签是否存在。这个设计把“变更”和“校验”耦合在一起,容易受到顺序和重新调用影响。更稳妥的方式是:

  • 由 Mutating Webhook 负责添加标签;
  • 由 Validating Webhook 检查最终对象中的业务条件;
  • 或让 B 自己根据最终字段推导条件,而不是依赖某个 Webhook 的实现细节。

十一、失败策略:failurePolicy 到底控制什么

Webhook 配置中的:

failurePolicy: Fail

表示当 Webhook 调用失败时拒绝请求。失败包括:

  • DNS 解析失败;
  • Service 没有可用 Endpoint;
  • TLS 握手失败;
  • 连接超时;
  • Webhook 返回不可解析响应;
  • Webhook 进程崩溃;
  • API Server 无法完成调用。

另一个取值是:

failurePolicy: Ignore

表示 Webhook 调用失败时忽略该 Webhook,继续处理请求。

这不是“业务拒绝”和“网络故障”的统一开关。需要区分:

情况 Webhook 返回
对象违反业务策略 allowed: false
Webhook 服务不可达,Fail API Server 拒绝请求
Webhook 服务不可达,Ignore API Server 忽略该 Webhook
Webhook 超时,Fail 请求失败
Webhook 超时,Ignore 继续处理,但策略可能未执行

什么时候使用 Fail

如果 Webhook 承担的是不可绕过的安全边界,例如:

生产环境禁止特权容器
所有镜像必须来自可信仓库

使用 Ignore 可能导致 Webhook 故障时违规对象进入集群,因此通常更接近 Fail 的语义。

代价是 Webhook 成为 API 写入路径上的硬依赖。只要它不可用,相关对象就无法创建或更新。

什么时候使用 Ignore

如果 Webhook 提供的是非关键增强能力,例如:

自动添加观测标签
可选的开发环境默认配置
非关键的成本分析信息

可以考虑 Ignore,但必须接受一个事实:故障期间该功能会静默失效。若业务依赖这些字段,Ignore 会把故障变成不完整对象,而不是显式失败。

failurePolicy 不是高可用方案。把它从 Fail 改成 Ignore 只能降低可用性故障对请求的阻断,不能修复 Webhook,也不能保证策略执行。


十二、超时、重试和请求放大

Webhook 使用 timeoutSeconds 指定单次调用的超时时间,取值范围和上限由 admissionregistration.k8s.io/v1 API 约束,当前稳定 API 的最大值为 30 秒。生产配置通常应使用明显小于最大值的超时,但具体数值取决于策略复杂度和可接受延迟。

需要注意:

  • API Server 可能因请求失败而重新处理或由客户端重试;
  • 控制器通常会自动重试失败的 API 请求;
  • 一个用户操作可能触发多个相关 API 写请求;
  • 多个 Webhook 叠加会放大总延迟;
  • timeoutSeconds 是单个 Webhook 调用的上限,不是整个 API 请求的总耗时保证。

如果单个请求依次经过 nn 个变更 Webhook,粗略上界可以写成:

Tadmissioni=1nti+TvalidationT_{\text{admission}} \leq \sum_{i=1}^{n} t_i + T_{\text{validation}}

其中:

  • tit_i 是第 ii 个串行变更 Webhook 的实际耗时;
  • TvalidationT_{\text{validation}} 是并行校验阶段中最慢路径及其调度开销。

这不是 Kubernetes 的精确性能公式,而是用于说明:串行变更 Webhook 会直接叠加延迟;并行校验则通常受最慢校验影响。

Webhook 不应在一次 Admission 请求中执行长时间外部扫描。更合理的设计是:

  1. Admission 只检查已有的可信结果或快速规则;
  2. 异步系统执行复杂扫描;
  3. 通过状态、标签或发布流程阻止未完成扫描的对象进入下一阶段。

十三、Webhook 的可用性设计

Webhook 的可用性应按“API Server 调用一个内部 HTTPS 服务”来设计,而不是按普通业务 HTTP 服务设计。

1. 多副本和 Service

至少需要考虑:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: image-policy
  namespace: admission-system
spec:
  replicas: 3
  selector:
    matchLabels:
      app: image-policy
  template:
    metadata:
      labels:
        app: image-policy
    spec:
      containers:
      - name: webhook
        image: registry.example.com/security/image-policy:v1.2.3
        ports:
        - name: https
          containerPort: 8443
        readinessProbe:
          httpGet:
            scheme: HTTPS
            path: /healthz
            port: https
---
apiVersion: v1
kind: Service
metadata:
  name: image-policy
  namespace: admission-system
spec:
  selector:
    app: image-policy
  ports:
  - name: https
    port: 443
    targetPort: https

这里的关键关系是:

ValidatingWebhookConfiguration.clientConfig.service
        ↓
Service
        ↓
Ready Endpoint
        ↓
Webhook Pod

Deployment 有三个副本并不自动意味着可用。还需要确认:

kubectl -n admission-system get deploy,pods,svc,endpointslice
kubectl -n admission-system get events --sort-by=.lastTimestamp

如果 Pod 没有通过 Readiness Probe,它可能不会进入 EndpointSlice。此时 Service 存在,但 API Server 仍然无法调用有效后端。

2. 拓扑和升级

生产集群通常还需要:

  • 将副本分散到不同节点或故障域;
  • 使用合适的 PodDisruptionBudget
  • 避免一次升级同时删除全部副本;
  • 确保 Webhook 自身的镜像拉取不依赖被它保护的路径;
  • 确保证书和配置在滚动升级期间兼容。

PDB 只能约束自愿驱逐,不能防止节点突然宕机,也不能保证 API Server 一定能访问 Webhook。

3. 避免循环依赖

一个危险架构是:

API Server
  → Admission Webhook
      → 调用 Kubernetes API
          → 再次触发同一个 Admission Webhook

如果 Webhook 处理创建 Pod 时又调用 API Server 创建或修改对象,就可能产生递归调用、死锁式等待或故障放大。

即使通过规则排除了自身资源,也要检查间接依赖:

Webhook → 创建 ConfigMap
ConfigMap → 被另一个全局 Webhook 匹配
另一个 Webhook → 等待第一个 Webhook

Webhook 应尽量:

  • 依赖本地内存缓存或只读缓存;
  • 避免在 Admission 请求期间创建 API 对象;
  • 使用 namespaceSelector 和资源规则排除自身命名空间及控制对象;
  • 不依赖正在被该 Webhook 保护的服务启动条件。

4. API Server 高可用不等于 Webhook 高可用

即使 API Server 有多个副本,如果它们都依赖同一个单副本 Webhook,整个写入路径仍然可能因 Webhook 故障而不可用。

可以把相关可用性近似理解为:

AwriteAapiserver×Awebhook×Anetwork×AcertificateA_{\text{write}} \approx A_{\text{apiserver}} \times A_{\text{webhook}} \times A_{\text{network}} \times A_{\text{certificate}}

这是帮助识别串联故障的简化模型,不是 Kubernetes 的正式 SLA 公式。它说明:增加 API Server 副本不会消除 Webhook 这一串联依赖。


十四、sideEffects 和 Dry Run

Webhook 必须声明 sideEffects,用于告诉 API Server 它是否会产生请求之外的副作用。

常见值:

  • None:没有副作用;
  • NoneOnDryRun:普通请求可能有副作用,但 Dry Run 请求不会有;
  • 其他历史值已不适合作为当前稳定设计。

如果 Webhook 声明:

sideEffects: None

它就必须能够安全处理:

metadata:
  annotations:
    ...

形式的对象预览请求而不发送外部通知、不扣费、不创建外部资产、不写入不可回滚系统。

例如,一个 Webhook 在每次 Admission 调用时都向外部计费系统创建订单,就不能声称没有副作用。因为客户端执行:

kubectl apply --dry-run=server -f pod.yaml

时,也可能触发该 Webhook。

一个实现正确的策略应区分:

dryRun=true:
    只计算是否允许,不改变外部状态
dryRun=false:
    才允许执行必要的外部动作

但更推荐将外部副作用从 Admission 路径移出,因为请求重试、超时和客户端重复提交会使“只执行一次”很难保证。


十五、Webhook 作用域必须尽量小

Webhook 配置匹配过宽会同时造成性能风险和可用性风险。

例如,下面的规则非常宽:

rules:
- apiGroups:
  - "*"
  apiVersions:
  - "*"
  operations:
  - "*"
  resources:
  - "*"
  scope: "*"

它可能拦截大量系统对象,包括:

  • 控制器创建和更新的对象;
  • Webhook 自身的 Deployment、Service 和 Secret;
  • Lease、EndpointSlice 等高频对象;
  • 状态更新;
  • 删除和子资源请求。

更精确的规则应明确:

rules:
- apiGroups:
  - ""
  apiVersions:
  - v1
  operations:
  - CREATE
  - UPDATE
  resources:
  - pods
  scope: Namespaced

如果策略只需要检查 Pod 创建,就不要匹配 DELETECONNECT、所有子资源或整个集群的所有资源。

同时可以使用:

namespaceSelector:
  matchExpressions:
  - key: policy.example.com/skip
    operator: DoesNotExist

排除 Webhook 自身所在命名空间。但排除标签必须受到保护,否则任何用户都可以给自己的命名空间打上绕过标签。标签是否可被普通用户修改,应通过 RBAC 和命名空间管理流程控制。


十六、内置策略、Webhook 和声明式策略的边界

实现准入策略通常有三条路径。

1. 内置 Admission 插件

适合 Kubernetes 已经定义好的通用问题:

  • Pod 安全;
  • 资源配额;
  • 命名空间生命周期;
  • 默认 ServiceAccount;
  • 默认 StorageClass;
  • LimitRange。

优点是控制面内置、故障面小、升级路径清晰。缺点是表达能力受固定插件行为限制。

2. ValidatingAdmissionPolicy

Kubernetes 提供基于 CEL 的声明式校验策略 API,用于将一部分校验逻辑直接声明在 Kubernetes 对象中,而不是运行独立 Webhook。

它适合表达字段级、对象级和部分关联对象校验,例如:

容器镜像必须来自 registry.example.com
metadata.labels["team"] 必须存在
spec.replicas 不得超过某个范围

具体 API 可用性、绑定方式和 CEL 能力必须以目标集群版本为准。不要假设所有云厂商旧版本都启用了同样的 API。

它不能替代所有 Webhook,尤其不适合:

  • 复杂外部系统查询;
  • 需要自定义网络协议的逻辑;
  • 大量状态缓存;
  • 复杂的对象变更;
  • 需要执行外部副作用的流程。

3. Mutating/Validating Webhook

适合需要自定义代码或外部服务的逻辑,但应承担更高的可用性和安全责任。

可以按以下因果关系选择:

规则能由内置插件稳定表达
    → 优先内置插件

规则主要是对象字段校验,且目标版本支持对应声明式 API
    → 考虑 ValidatingAdmissionPolicy

规则需要注入、复杂计算或企业系统集成
    → 使用 Webhook,并严格控制范围和故障路径

这里的“优先”是工程取舍,不是 Kubernetes API 的强制规范。


十七、一个完整策略例子:禁止不可信镜像

目标:

dev 命名空间中,Pod 的所有容器镜像必须来自 registry.example.com

第一步:创建测试命名空间

kubectl create namespace dev
kubectl label namespace dev admission.example.com/enabled=true

前置条件是集群中已经部署并注册了相应 Validating Webhook,并且 Webhook 的 namespaceSelector 匹配该标签。

第二步:提交违规对象

apiVersion: v1
kind: Pod
metadata:
  name: bad-image
  namespace: dev
spec:
  containers:
  - name: app
    image: docker.io/library/nginx:1.27

执行:

kubectl apply -f bad-image.yaml

预期结果是请求失败,例如:

Error from server: admission webhook "image-policy.example.com" denied the request:
container app uses disallowed image registry docker.io

对象不会被持久化:

kubectl -n dev get pod bad-image

预期:

Error from server (NotFound): pods "bad-image" not found

第三步:提交允许对象

apiVersion: v1
kind: Pod
metadata:
  name: good-image
  namespace: dev
spec:
  containers:
  - name: app
    image: registry.example.com/base/nginx:1.27

如果镜像策略通过,Admission 允许请求,之后是否能成功运行还取决于:

  • 节点能否访问镜像仓库;
  • 镜像凭据是否配置;
  • 调度资源是否足够;
  • 镜像本身是否可用。

因此,“准入允许”不等于“Pod 一定变成 Running”。


十八、Pod Security Admission 与 SecurityContext 的关系

Pod Security Admission 检查的是 Pod 是否符合所选 Pod Security Standard;securityContext 是对象中表达安全配置的字段。

例如:

apiVersion: v1
kind: Pod
metadata:
  name: restricted-demo
  namespace: dev
spec:
  securityContext:
    runAsNonRoot: true
    seccompProfile:
      type: RuntimeDefault
  containers:
  - name: app
    image: registry.example.com/base/app:1.0
    securityContext:
      allowPrivilegeEscalation: false
      capabilities:
        drop:
        - ALL

这些字段分别可能影响:

  • 是否以非 root 用户运行;
  • 是否允许权限提升;
  • Linux capabilities 是否被删除;
  • seccomp 是否使用运行时默认配置。

需要区分 Pod 级和容器级 SecurityContext:

spec.securityContext
    → Pod 或容器的默认安全上下文

spec.containers[].securityContext
    → 单个容器的覆盖配置

即使写了 runAsNonRoot: true,镜像中的实际用户配置仍可能导致运行失败。例如镜像没有可用的非 root 用户,准入阶段可能通过,但 kubelet 启动容器时无法满足约束。

因此:

PSA 通过
≠
SecurityContext 在运行时一定有效
≠
容器应用一定能正常启动

PSA 是通用安全基线;企业还可以使用 Webhook 或 ValidatingAdmissionPolicy 补充镜像来源、标签、资源和组织规则。


十九、失败表现和诊断路径

1. Webhook 不可达

常见错误:

failed calling webhook "image-policy.example.com":
dial tcp: connect: connection refused

可能原因:

  • Service 没有 Endpoint;
  • Pod 没有通过 Readiness Probe;
  • Service 端口与 targetPort 不一致;
  • Webhook 监听的是 HTTP 而 API Server 使用 HTTPS;
  • NetworkPolicy 阻止控制面访问;
  • 托管集群中控制面到集群网络的路径有额外限制。

诊断:

kubectl -n admission-system get svc image-policy -o yaml
kubectl -n admission-system get endpointslice \
  -l kubernetes.io/service-name=image-policy
kubectl -n admission-system get pods -l app=image-policy -o wide
kubectl -n admission-system logs deploy/image-policy

不要只检查 Pod 是否为 RunningRunning 不代表服务端口正确监听,也不代表它已经进入 Service Endpoint。

2. TLS 失败

常见错误:

x509: certificate signed by unknown authority

或:

x509: certificate is valid for webhook.admission-system.svc,
not image-policy.admission-system.svc

分别通常表示:

  • caBundle 不包含签发服务端证书的 CA;
  • 服务端证书的 SAN 不包含 API Server 访问的 Service DNS 名称。

检查配置:

kubectl get validatingwebhookconfiguration image-policy.example.com -o yaml

不能使用跳过 TLS 验证的方式解决问题。Admission Webhook 是控制面安全边界,跳过验证会允许中间人伪造策略服务。

3. Webhook 业务拒绝

如果错误包含:

denied the request

而不是 failed calling webhook,通常说明 Webhook 已经成功收到请求,并主动返回了 allowed: false。这时应查看:

  • API Server 返回的 reasonmessage
  • Webhook 日志中的 AdmissionReview UID;
  • 实际对象是否经过其他 Mutating Webhook 修改;
  • 使用的 API 版本、子资源和操作是否符合预期。

4. API Server 卡顿或写入大量超时

应关注:

  • Webhook 调用延迟;
  • Webhook 连接错误率;
  • API Server 的请求延迟和拒绝数;
  • Webhook Pod CPU、内存和连接数;
  • EndpointSlice 是否频繁变化;
  • 证书是否接近过期;
  • 是否匹配了过多系统对象。

审计日志可以帮助确认谁发起了请求、请求了什么资源以及结果是什么,但审计策略本身是否记录 AdmissionReview 细节取决于配置。Webhook 应在日志中记录:

AdmissionReview UID
operation
resource
namespace
name
decision
reason
latency

同时避免记录完整 Secret、Token 或其他敏感字段。


二十、典型反例

反例一:为所有资源设置 failurePolicy: Fail

这会让一个只需要检查 Pod 镜像的 Webhook 变成整个集群所有写请求的单点依赖。Webhook 证书过期或 Service 故障后,可能连修复 Webhook 所需的 Deployment、Secret、Service 更新都被阻断。

改进方法:

  • 只匹配必要资源和操作;
  • 排除自身命名空间;
  • 为恢复流程预留不依赖该 Webhook 的路径;
  • 在变更 Webhook 配置前测试证书和 Endpoint;
  • 必要时先临时将故障 Webhook 改为 Ignore,恢复服务后再改回 Fail

最后一项需要严格的控制面管理员权限,并且会产生安全窗口,不能作为日常绕过方式。

反例二:Mutating Webhook 每次都追加字段

结果是同一对象因重试或重新调用不断膨胀,最终出现重复 sidecar、重复 Volume 或不可预测的环境变量。

改进方法是设计幂等变更,并为已存在但冲突的配置返回明确错误。

反例三:用客户端可修改标签作为强制安全边界

例如策略规则是:

namespaceSelector:
  matchLabels:
    security.example.com/enforced: "true"

如果租户可以修改命名空间标签,它就可以删除该标签绕过规则。选择器本身只是匹配机制,不自动提供标签不可篡改保证。

反例四:Webhook 在准入期间依赖外部扫描系统

如果每次创建 Pod 都同步等待镜像扫描,扫描系统的延迟和故障会直接传导到 API Server。更严重时,控制器持续重试,导致请求洪泛。

适合 Admission 的逻辑应是快速、可预测、可重复的。复杂扫描应异步化,并在发布流程或后续控制器中执行阻断。


二十一、变更 Webhook 和安全策略的部署顺序

新增一个强制拒绝型 Webhook 时,建议按状态转换验证,而不是直接切换到生产阻断:

未注册
  ↓
注册但只匹配测试命名空间
  ↓
failurePolicy=Ignore,观察调用和延迟
  ↓
测试命名空间 failurePolicy=Fail
  ↓
扩大匹配范围
  ↓
生产范围 failurePolicy=Fail

每一步都应验证:

kubectl apply --dry-run=server -f allowed.yaml
kubectl apply --dry-run=server -f denied.yaml
kubectl create -f allowed.yaml
kubectl create -f denied.yaml

--dry-run=server 会请求 API Server 执行服务端默认值、准入和校验,但不持久化对象。它适合验证:

  • Webhook 是否被匹配;
  • Mutation 是否出现;
  • 策略是否拒绝;
  • Dry Run 路径是否正常。

但 Dry Run 不能证明真实创建后的控制器、调度器和 kubelet 行为,也不能替代一次受控的真实创建测试。


二十二、如何判断一个策略是否真的生效

不能只看 Webhook Configuration 已经创建。至少需要验证四层:

配置层

kubectl get validatingwebhookconfiguration
kubectl get mutatingwebhookconfiguration
kubectl get validatingadmissionpolicy
kubectl get validatingadmissionpolicybinding

目标是确认规则、选择器、failurePolicy、证书和版本配置符合预期。

网络层

kubectl -n admission-system get svc,endpointslice
kubectl -n admission-system get pods -o wide

确认 API Server 能通过 Service 找到 Ready 后端。对于某些托管控制面,控制面到 Pod 网络的可达性还需要参考云厂商网络模型。

语义层

提交至少三类对象:

  1. 应当允许的对象;
  2. 应当拒绝的对象;
  3. 通过 Mutation 后应当允许的对象。

例如:

非法镜像 → 拒绝
合法镜像 → 允许
缺少标签但可自动注入 → 变更后允许

故障层

在非生产环境模拟:

  • 删除 Webhook Pod;
  • 暂停 Endpoint;
  • 使用错误 CA;
  • 让服务端口不监听;
  • 观察 FailIgnore 的差异;
  • 恢复配置并确认已有 API 请求恢复。

这一步决定了团队是否真正理解该 Webhook 的可用性代价。


二十三、弃用、版本偏差和云厂商差异

准入相关行为受以下因素影响:

  1. Kubernetes 主版本;
  2. API Server 默认启用的插件集合;
  3. 发行版或云厂商控制面配置;
  4. admissionregistration.k8s.io/v1 API 的具体字段支持;
  5. Pod Security Standards 对应的版本标签;
  6. CEL、ValidatingAdmissionPolicy 等能力是否在目标集群可用;
  7. Webhook 使用的 AdmissionReview 版本和对象版本。

因此,生产部署前应在目标集群执行:

kubectl version
kubectl api-resources | grep -i admission
kubectl explain validatingwebhookconfiguration.webhooks
kubectl explain validatingadmissionpolicy

kubectl explain 反映的是当前集群 OpenAPI 信息,比直接复制其他版本文章中的字段更可靠。

不要依赖已经移除或历史行为:

  • 已弃用的 PSP 不应作为新设计基础;
  • admissionregistration.k8s.io/v1beta1 不应作为当前稳定配置目标;
  • 历史上某些插件的默认启用状态不能推断当前集群;
  • 云厂商可能禁止直接修改控制面 Admission 插件;
  • 服务网格或安全产品可能自动注入 Webhook,造成额外准入链和版本耦合。

结语:准入控制的真正边界

Kubernetes Admission Control 的核心不是“再加一道校验”,而是把对象从客户端意图转换为可持久化状态:

认证身份
  → 授权动作
  → 变更候选对象
  → 执行安全与业务策略
  → 在满足全部条件后写入 etcd

内置插件适合通用且控制面原生的约束;Pod Security Admission 提供 Pod 安全基线;声明式策略适合可表达的字段校验;Webhook 适合自定义变更和复杂业务规则,但会把网络服务、TLS、延迟和故障处理引入 API Server 的关键路径。

一个准入方案只有同时满足以下条件,才算完整:

策略有效=匹配正确逻辑正确变更幂等失败语义明确服务可用故障可恢复\text{策略有效} = \text{匹配正确} \land \text{逻辑正确} \land \text{变更幂等} \land \text{失败语义明确} \land \text{服务可用} \land \text{故障可恢复}

只验证“正常请求能通过”是不够的。真正需要验证的是:策略是否覆盖了目标对象、是否能拒绝违规对象、是否能处理重试和 Dry Run,以及当 Webhook、证书、网络或控制器本身发生故障时,集群会选择安全地拒绝,还是有意识地降级。


系列导航与关联阅读

官方资料

本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。