Kubernetes 集群中很多风险配置并不是由攻击者直接引入的,而是来自开发者误操作、CI 流水线参数错误或临时调试留下的后门。要避免这些风险真正影响生产环境,不能只依赖周期性的合规扫描,必须在 API 请求写入 etcd 之前就完成校验。实现这一点的核心是动态准入控制,尤其是 ValidatingAdmissionWebhook。

一、拦截点为什么放在准入链路上
Kubernetes API Server 收到创建或更新资源的请求后,会先完成用户认证和 RBAC 授权,然后进入准入控制阶段。内置的准入控制器会做很多默认行为,比如为 Pod 自动添加默认服务账号,而动态准入 webhook 则允许我们插入自己的校验逻辑。相比在 CI 或 GitOps 仓库里做静态检查,这个位置有两个明显优势:一是所有客户端入口共享同一套策略,kubectl、helm、控制器和 CI 工具都无法绕过;二是请求被拒绝时资源根本不会写入 etcd,不会留下一个需要事后清理的违规对象。
动态准入包含 MutatingAdmissionWebhook 和 ValidatingAdmissionWebhook。前者可以修改对象,例如注入 sidecar;后者只能返回允许或拒绝。实时阻断通常只需要 ValidatingWebhook,因为它的语义更简单,不容易因为多个 mutating webhook 的顺序而产生副作用。校验 webhook 的调用是同步的,API Server 会等待它返回,所以策略执行时间必须严格控制,否则会拖慢所有资源操作。
下面的 ValidatingWebhookConfiguration 展示了如何注册一个校验端点,同时配置它监听 Pod 创建或更新操作。
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: pod-policy.ipipp.com
webhooks:
- name: validate-pods.ipipp.com
clientConfig:
service:
name: policy-webhook
namespace: policy-system
path: /validate/pod
caBundle: BASE64_ENCODED_CA_CERT
rules:
- operations: ["CREATE","UPDATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
failurePolicy: Fail
matchPolicy: Equivalent
namespaceSelector:
matchLabels:
policy: enabled
sideEffects: None
admissionReviewVersions: ["v1"]
timeoutSeconds: 5
上例中的 failurePolicy: Fail 表示 webhook 服务不可用时,API Server 会直接拒绝匹配的请求。对于安全强相关的策略,这是正确的选择,因为不可观测或不可校验不应变成默认放行。但代价是 webhook 自身故障会扩大影响面。如果策略只用于成本控制或最佳实践提示,可以选择 Ignore,同时在监控中持续检查 webhook 就绪状态。
timeoutSeconds 默认是 10 秒,5 秒适用于大多数纯内存策略。要避免在 webhook 里调用外部服务做同步查询,否则超时会导致 API 请求失败。对于需要查询外部数据的场景,可以把结果缓存在本地,或者改用异步审计。
二、使用 OPA Gatekeeper 实现策略即代码
OPA Gatekeeper 将通用策略引擎 OPA 包装成 Kubernetes 动态准入控制器,并通过 CRD 提供 ConstraintTemplate 和 Constraint 两个抽象。前者定义通用的 Rego 校验逻辑和参数结构,后者提供具体参数并绑定到命名空间或标签选择器。这样平台团队编写一次模板,业务团队可以在自己的命名空间里实例化不同阈值,而不需要每一条策略都写一个 webhook 服务。
第一个典型场景是禁止使用 latest 镜像标签。下面是 ConstraintTemplate 中的 Rego:
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sdisallowedtags
spec:
crd:
spec:
names:
kind: K8sDisallowedTags
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sdisallowedtags
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
image := container.image
endswith(image, ":latest")
msg := sprintf("容器 %v 不允许使用 latest 镜像标签", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
image := container.image
endswith(image, ":latest")
msg := sprintf("init 容器 %v 不允许使用 latest 镜像标签", [container.name])
}
该模板会检查 Pod 中普通容器和 init 容器的镜像名是否以 :latest 结尾。如果命中,就生成一条违规消息。接下来用 Constraint 把这条策略绑定到指定命名空间:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sDisallowedTags
metadata:
name: block-latest-images
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- "production"
- "staging"
Constraint 中的 match.kinds 只匹配 Pod 对象,并通过 namespaces 限制在 production 和 staging。如果开发命名空间需要更宽松的策略,可以不复用这个实例。Gatekeeper 在接收到 AdmissionReview 后会把对象转换为 Rego 输入,执行模板中所有 violation 规则,只要任一条产生结果,就返回拒绝响应,API Server 再把拒绝原因显示给用户。
另一个高频需求是要求 Pod 必须声明资源请求和上限。Rego 可以分别检查 resources.requests 和 resources.limits 中的 CPU 与内存配置。
package requiredresources
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.cpu
msg := sprintf("容器 %v 缺少 CPU 上限配置", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("容器 %v 缺少内存上限配置", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.requests.cpu
msg := sprintf("容器 %v 缺少 CPU 请求配置", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.requests.memory
msg := sprintf("容器 %v 缺少内存请求配置", [container.name])
}
这样的规则可以避免 Pod 无限制占用节点资源。不过在实际使用中,还要考虑系统命名空间里的 Pod,它们通常不应被业务策略约束。如果一刀切匹配所有命名空间,可能会把 kube-system 中的组件拦住,因此建议先使用 match.namespaces 或 namespaceSelector 排除系统空间。
三、把违规拦截事件变成可观测告警
阻断策略生效后,用户会看到类似 admission webhook "validation.gatekeeper.sh" denied the request 的错误。但对平台团队来说,单条拒绝信息只停留在 API 客户端,无法形成趋势分析。需要把策略违规事件从 webhook 服务器输出到集中日志或指标系统,再接告警。
Gatekeeper 会通过 Prometheus 指标暴露约束状态和违规数量。gatekeeper_violations_total 等指标可供 Prometheus 抓取。下面是一条告警规则,它检测五分钟内是否出现新的违规,并且持续一分钟才触发,降低抖动。
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: gatekeeper-violation-alerts
namespace: policy-system
spec:
groups:
- name: gatekeeper.rules
rules:
- alert: ConstraintViolation
expr: increase(gatekeeper_violations_total[5m]) > 0
for: 1m
labels:
severity: warning
team: platform
annotations:
summary: 检测到新的策略违规
description: 最近 5 分钟违规数量增加了 {{ $value }} 次,请检查对应命名空间。
如果不想依赖 Prometheus,也可以直接采集 webhook 容器的结构化日志。建议在日志中记录对象命名空间、名字、用户、策略名和违规原因,例如 JSON 字段。使用 Fluent Bit 或 Loki 聚合后,配置告警规则匹配 level=deny 的日志,并推送到钉钉、企业微信或邮件。关键是要保留 user 字段,方便定位是哪个账号触发了策略。
另一种更直接的方式是在 webhook 服务中增加通知钩子。当校验失败返回 allowed: false 前,异步调用告警接口。但要注意不要在请求主流程中同步等待通知系统,否则可能拖慢 API 响应。应当写入本地队列或直接发送到内存 channel,由后台 worker 处理。
如果策略数量较多,建议在告警信息中带上 constraintName 和 enforcementAction,这样接收者能一眼区分是实际阻断还是仅审计。
四、dry-run 和 failurePolicy 的取舍
策略上线最怕误伤。即使 Rego 逻辑简单,也可能因为未考虑某些控制面资源而把系统组件拦住。例如一个要求所有容器必须设置资源限制的策略,如果意外匹配到 kube-system 下的系统 Pod,可能导致节点组件无法更新。为了避免这类问题,新策略应先使用 enforcementAction: dryrun 上线。
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sDisallowedTags
metadata:
name: block-latest-images
spec:
enforcementAction: dryrun
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
在 dryrun 模式下,Gatekeeper 不会拒绝请求,但会在约束对象的 status.violations 中记录违规详情。平台团队可以观察一段时间,确认没有大量误报后,再把 enforcementAction 改成 deny。对于已经存在的存量资源,dryrun 也能配合审计功能发现历史违规,但不会自动驱逐已有 Pod。
另一个容易忽略的地方是 failurePolicy。它决定 webhook 服务故障时 API Server 是否放行。把每个 webhook 都设为 Ignore 并不稳妥,因为这等于给策略一致性开了一个口子:只要 webhook 不可用,违规资源就能进入集群。更合理的做法是分层:核心安全策略使用 Fail,并保证 webhook 多副本、自动扩缩容和独立故障域;非核心最佳实践策略可以使用 Ignore,但要配置 webhook 健康检查和可用性告警,确保故障期间有记录可查。
此外,webhook 的 namespaceSelector 也可以用来先对少量命名空间启用实时阻断,再逐步扩大范围。渐进式放量比一次性全局启用更安全,也更容易定位某个业务特有问题。
通过把策略校验放在准入链路上的正确位置、使用 Gatekeeper 管理策略实例,并把违规事件接入指标告警,集群安全可以从事后补救转变成实时阻断。关键是控制好 dry-run、failurePolicy 和超时配置,在安全和可用性之间找到适合自己团队的位置。
Kubernetes策略引擎准入控制修改时间:2026-09-18 11:11:27