准入控制深度:Validating/Mutating Webhook 与策略执行

系统梳理 Kubernetes 准入控制链——ValidatingAdmissionWebhook 与 MutatingAdmissionWebhook 的配置字段、failurePolicy 与超时语义、Webhook 服务端的 TLS 与证书轮换、OPA Gatekeeper 与 Kyverno 策略引擎对比,以及准入链顺序、幂等性、高可用与排错的生产实践。

RBAC 只能回答「谁能调这个 API」,回答不了「这个 Pod 是否允许跑特权容器」。准入控制(Admission Control)才是 API Server 上的策略执行点:它在对象落库之前拦截请求,可以改写字段(Mutating)也可以拒绝请求(Validating)。本文从准入链的执行顺序讲起,深入 Webhook 配置字段语义、服务端实现与证书轮换,并对比 OPA Gatekeeper 与 Kyverno 两种策略引擎,最后给出高可用、降噪与排错的生产清单。


目录


1. 准入控制基础与准入链

1.1 请求在 API Server 中的生命周期

一个写请求进入 API Server 后,依次经过认证、鉴权、准入、持久化四个阶段。准入阶段又分两步:变更(Mutating)在前,校验(Validating)在后。这个顺序不是随意设计的——只有先允许策略改写对象,校验阶段看到的才是「最终形态」,避免校验器对即将被改写的字段做无意义判断。

kubectl apply
  -> [认证 Authentication]   你是谁
  -> [鉴权 Authorization]    RBAC / Node / Webhook 鉴权
  -> [变更准入 Mutating]     可 patch 对象(注入 sidecar、补默认值)
  -> [对象 Schema 校验]      OpenAPI schema validation
  -> [校验准入 Validating]   只返回 allow / deny / warn
  -> [etcd 持久化]

1.2 内置准入插件与动态准入

内置插件(编译进 API Server,如 NamespaceLifecycle、LimitRanger、ResourceQuota、PodSecurity)与动态准入(外部 Webhook)的区别在于能否独立升级。内置插件随集群版本走,动态准入则可以独立部署、独立发布策略。

维度内置插件动态准入 Webhook
部署方式编译进 API Server独立 Deployment + Service
升级节奏随集群独立
表达能力固定逻辑任意代码 / DSL
失败影响几乎不会失败服务不可用会导致写入失败

确认动态准入 API 可用:

kubectl api-versions | grep admissionregistration.k8s.io
kubectl api-resources | grep admissionregistration

2. ValidatingWebhookConfiguration 详解

2.1 最小可用配置

ValidatingWebhookConfiguration 描述「哪些请求要发给哪个服务做校验」。

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: pod-policy.example.com
webhooks:
  - name: pod-policy.example.com
    admissionReviewVersions: ["v1"]
    sideEffects: None
    failurePolicy: Fail
    timeoutSeconds: 5
    clientConfig:
      service:
        name: policy-webhook
        namespace: policy-system
        path: /validate-pods
        port: 443
      caBundle: <base64 编码的 CA 证书>
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
        scope: Namespaced
    namespaceSelector:
      matchExpressions:
        - key: policy.example.com/exempt
          operator: DoesNotExist

2.2 关键字段语义

failurePolicy 决定 Webhook 调用失败(网络不通、超时、返回 5xx)时的行为:

取值语义适用场景
Fail调用失败即拒绝请求安全强约束,宁可不可用也不放行
Ignore调用失败则放行审计类、告警类策略

sideEffects 必须显式声明,取值 None / NoneOnDryRun。若声明 None 却在处理 dry-run 请求时产生副作用,会导致 API Server 拒绝该配置。

matchPolicy 控制版本等价匹配。取 Equivalent 时,即使请求通过 apps/v1beta1 这类旧版本发出,只要与规则里声明的版本等价也会触发;取 Exact 则必须版本字面一致。

timeoutSeconds 默认 10 秒,最大 30 秒。这是 API Server 的等待上限,超时即按 failurePolicy 处理。

2.3 规则匹配三要素

rules 决定请求类别(操作 + 资源 + 版本),namespaceSelector 决定命名空间范围,objectSelector 决定对象标签范围。三者是与关系。常见的坑是把 namespaceSelector 当作匹配命名空间名字——它匹配的是 Namespace 对象的标签:

kubectl label ns kube-system policy.example.com/exempt=true

3. MutatingWebhookConfiguration 详解

3.1 与 Validating 的差异

Mutating 多出 reinvocationPolicy 字段,取值 Never(默认)或 IfNeeded。当多个 Mutating Webhook 串联时,前一个 Webhook 的改写可能让后一个 Webhook 的改写失效;IfNeeded 允许 API Server 在所有变更完成后再调用一次那些声明了该策略的 Webhook。

3.2 JSONPatch 响应格式

Mutating Webhook 通过返回 JSONPatch 表达改写意图,patchType 为 JSONPatch,patch 是 base64 编码的补丁数组:

{
  "response": {
    "uid": "<与请求一致>",
    "allowed": true,
    "patchType": "JSONPatch",
    "patch": "W3sib3AiOiJhZGQiLCJwYXRoIjoiL21ldGFkYXRhL2xhYmVscy9pbmplY3RlZCJ9XQ=="
  }
}

该 base64 解码后是 [{"op":"add","path":"/metadata/labels/injected","value":"true"}]。

3.3 幂等性是硬要求

因为 reinvocationPolicy: IfNeeded 会让 Webhook 被调用多次,补丁必须幂等。向 containers 数组追加元素这种操作天然不幂等,第二次调用就会追加重复项。正确做法是先检查目标是否存在:

// 伪代码:先判断再注入,保证幂等
for _, c := range pod.Spec.Containers {
    if c.Name == "sidecar" {
        return allowWithoutPatch()
    }
}
patches = append(patches, addSidecarPatch())

常见场景包括 Sidecar 注入(Istio / Linkerd)、默认值填充(resources、securityContext)、标签规范化(team、cost-center)以及镜像地址改写(公网镜像改写为内网仓库)。


4. Webhook 服务端实现与 TLS 配置

4.1 为什么必须 HTTPS

API Server 只允许通过 HTTPS 调用 Webhook,且会校验服务端证书是否被 caBundle 中的 CA 签发,因此自签证书 + 把 CA 写进 caBundle 是最常见方案:

openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
  -keyout ca.key -out ca.crt -subj "/CN=policy-ca"
openssl req -newkey rsa:2048 -nodes -keyout tls.key -out tls.csr \
  -subj "/CN=policy-webhook.policy-system.svc"
openssl x509 -req -in tls.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out tls.crt -days 3650 \
  -extfile <(printf "subjectAltName=DNS:policy-webhook.policy-system.svc")

CN / SAN 必须匹配 clientConfig.service 拼出的 DNS 名:<service>.<namespace>.svc。只写短名或不写 SAN 会在新版 Go 中被拒绝。

4.2 证书轮换

自签证书会过期,过期那天整个集群的写入可能被 failurePolicy: Fail 拦死。生产上推荐用 cert-manager 自动签发并轮换:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: policy-webhook-cert
  namespace: policy-system
spec:
  secretName: policy-webhook-tls
  dnsNames:
    - policy-webhook.policy-system.svc
    - policy-webhook.policy-system.svc.cluster.local
  issuerRef:
    name: selfsigned-issuer
    kind: ClusterIssuer

配合 cert-manager.io/inject-ca-from 注解,让 caBundle 自动注入到 Webhook 配置里,避免手工维护 base64。

4.3 服务端骨架

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    var review admissionv1.AdmissionReview
    if err := json.Unmarshal(body, &review); err != nil {
        http.Error(w, "bad request", http.StatusBadRequest)
        return
    }
    resp := &admissionv1.AdmissionResponse{UID: review.Request.UID, Allowed: true}
    resp.Result = validate(review.Request)
    json.NewEncoder(w).Encode(admissionv1.AdmissionReview{TypeMeta: review.TypeMeta, Response: resp})
}

必须原样回填 review.Request.UID,否则 API Server 无法把响应与请求配对,会一直等到超时。


5. 策略引擎对比:OPA Gatekeeper 与 Kyverno

5.1 Gatekeeper:基于 Rego 的约束框架

Gatekeeper 把 OPA 引入集群,引入两层抽象:ConstraintTemplate(定义策略模板与参数 schema)与 Constraint(模板实例,绑定参数与匹配范围)。

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequiredlabels
        violation[{"msg": msg}] {
          provided := {l | input.review.object.metadata.labels[l]}
          required := {l | l := input.parameters.labels[_]}
          missing := required - provided
          count(missing) > 0
          msg := sprintf("缺少必需标签: %v", [missing])
        }

Rego 表达能力强,能跨对象查询(如「这个 Ingress 的 host 是否已被占用」),但学习曲线陡峭。

5.2 Kyverno:Kubernetes 原生 YAML 策略

Kyverno 直接用 Kubernetes 资源描述策略,validate / mutate / generate 三类规则覆盖绝大多数场景,无需学习新语言。

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-privileged
spec:
  validationFailureAction: Enforce
  rules:
    - name: no-privileged
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "禁止特权容器"
        pattern:
          spec:
            containers:
              - securityContext:
                  privileged: "false"

5.3 选型对比

维度GatekeeperKyverno
策略语言RegoKubernetes YAML
学习成本高低
跨对象查询强中等(context 变量)
变更能力支持(Assign/Modify)原生 mutate
适合团队平台组、有 Rego 经验应用团队自助

6. 准入链的顺序与幂等性

6.1 多个 Webhook 的执行顺序

同一类 Webhook 之间没有稳定顺序保证。API Server 会按名称排序后并发调用,因此不要设计「Webhook A 一定在 B 之前执行」的依赖。若确实需要顺序,只能合并到同一个 Webhook 内,或通过 reinvocationPolicy 间接协调。

6.2 变更与校验的交互

由于 Mutating 先于 Validating,校验策略看到的对象已经被改写。这带来一个常见困惑:策略要求「必须显式声明 resources」,但 Mutating Webhook 已经帮忙补了默认值,导致策略永远通过。解决办法是让校验策略检查注解或标签(如 policy.example.com/resources-defaulted: "true")而不是检查被改写后的字段。

6.3 幂等性检查清单

□ 补丁是否可能重复追加数组元素
□ 补丁是否覆盖了其他 Webhook 刚写入的字段
□ reinvocationPolicy=IfNeeded 时逻辑是否仍正确
□ 对已存在对象执行 UPDATE 时是否重复注入

7. 高可用与性能:超时、失败策略与缓存

7.1 可用性设计

Webhook 是集群写入路径上的强依赖。策略服务不可用且 failurePolicy: Fail 时,匹配到的资源将无法创建。因此需要多副本 + 跨节点反亲和:

spec:
  replicas: 3
  template:
    spec:
      affinity:
        podAntiAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            - topologyKey: kubernetes.io/hostname
              labelSelector:
                matchLabels:
                  app: policy-webhook

跨节点反亲和 + PodDisruptionBudget + 就绪探针,是保底三件套。

7.2 性能优化

准入是同步调用,每次写请求都要走一遍。优化手段:

手段说明
缩小匹配范围用 namespaceSelector 排除 kube-system 等高频命名空间
缩短超时timeoutSeconds 设 3~5 秒,避免慢请求堆积
本地缓存缓存 ConfigMap、Namespace 等只读数据
预留资源Webhook 服务预留 CPU,避免节流导致超时

7.3 失败策略的正确选择

安全强约束(禁止特权、禁止 hostPath)用 Fail;审计类(建议加标签)用 Ignore。永远不要给所有策略统一设 Fail,那等于把集群写入的可用性押在自研服务上。


8. 可观测性与排错

8.1 关键指标

admission_webhook_request_total{webhook, allowed}   请求量与放行率
admission_webhook_latency_seconds                   准入耗时分布
admission_webhook_rejection_total{reason}           拒绝原因分布
apiserver_admission_webhook_fail_open_count         fail-open 次数

apiserver_admission_webhook_fail_open_count 一旦非零,说明有请求因 Webhook 不可用而被放行,属于高危信号。

8.2 常见报错与定位

kubectl -n kube-system logs -l component=kube-apiserver --tail=200 | grep -i admission
kubectl -n policy-system port-forward svc/policy-webhook 8443:443
curl -k https://localhost:8443/validate-pods
kubectl get validatingwebhookconfiguration pod-policy.example.com \
  -o jsonpath='{.webhooks[0].clientConfig.caBundle}' | wc -c
报错原因
x509 certificate signed by unknown authoritycaBundle 与证书不匹配
context deadline exceeded超时,服务慢或不可达
no endpoints available for serviceService 无就绪 Pod

改动策略前先用 kubectl apply --dry-run=server -f pod.yaml 验证,它会真实走一遍准入链但不落库,是验证策略最安全的方式。


9. 生产最佳实践

9.1 落地 Checklist

□ 所有 Webhook 有明确的 namespaceSelector 豁免(至少 kube-system)
□ 证书由 cert-manager 自动轮换,caBundle 不手工维护
□ 安全强约束用 failurePolicy=Fail,审计类用 Ignore
□ Webhook 服务 3 副本 + 反亲和 + PDB + 就绪探针
□ timeoutSeconds 控制在 5 秒内
□ 变更补丁幂等,可安全重入
□ 策略纳入 Git,走 GitOps 评审后发布
□ 监控 fail-open 计数与准入延迟 P99

9.2 常见坑与对策

坑现象对策
证书过期全集群写入被拒cert-manager 自动轮换 + 到期告警
caBundle 不匹配x509 unknown authority用 inject-ca-from 自动注入
补丁不幂等重复注入 sidecar注入前先判断目标是否存在
Webhook 阻塞删除Namespace 卡在 Terminating对 DELETE 谨慎匹配或直接豁免
UID 未回填请求一直等到超时响应原样回填 request.uid

小结

准入控制是集群策略执行的唯一同步拦截点:Mutating 负责改写与补默认值,Validating 负责最终裁决,两者的顺序不可颠倒,且同一阶段内不保证执行顺序。落地时的核心权衡是可用性与约束力的平衡——failurePolicy: Fail 能守住安全底线,但必须配套多副本、反亲和与证书自动轮换,否则一次证书过期就可能演变成全集群写入中断。选型上,Rego 表达力强但门槛高,Kyverno 上手快且原生支持 mutate/generate;无论选哪种,都请把策略纳入 Git、用 --dry-run=server 预演、监控 fail-open 计数,让策略演进和业务迭代一样可控。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「云原生」更多文章

  1. 调度均衡:Descheduler 与资源碎片整理
  2. Cluster API 与声明式集群生命周期管理
  3. 运行时安全:Falco/Tetragon 与 eBPF 检测实战