RBAC 只能回答「谁能调这个 API」,回答不了「这个 Pod 是否允许跑特权容器」。准入控制(Admission Control)才是 API Server 上的策略执行点:它在对象落库之前拦截请求,可以改写字段(Mutating)也可以拒绝请求(Validating)。本文从准入链的执行顺序讲起,深入 Webhook 配置字段语义、服务端实现与证书轮换,并对比 OPA Gatekeeper 与 Kyverno 两种策略引擎,最后给出高可用、降噪与排错的生产清单。
目录
- 1. 准入控制基础与准入链
- 2. ValidatingWebhookConfiguration 详解
- 3. MutatingWebhookConfiguration 详解
- 4. Webhook 服务端实现与 TLS 配置
- 5. 策略引擎对比:OPA Gatekeeper 与 Kyverno
- 6. 准入链的顺序与幂等性
- 7. 高可用与性能:超时、失败策略与缓存
- 8. 可观测性与排错
- 9. 生产最佳实践
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 选型对比
| 维度 | Gatekeeper | Kyverno |
|---|---|---|
| 策略语言 | Rego | Kubernetes 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 authority | caBundle 与证书不匹配 |
| context deadline exceeded | 超时,服务慢或不可达 |
| no endpoints available for service | Service 无就绪 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 计数,让策略演进和业务迭代一样可控。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。