jimyag's Blog

Kubernetes 准入控制:安全策略、Webhook 与失败影响

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

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

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

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

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

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

  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 按匹配结果调用修改 Webhook,并把变更应用到本次请求的对象。后续修改器看到的内容可能已经包含前面的补丁。

正在加载 GitHub 文件…

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

validatingDispatcher.Dispatch 收集匹配的 Webhook,随后启动 goroutine 并发调用,并等待结果汇总。校验器不修改对象,不应依赖另一个校验器先运行,也不能从本次日志顺序推出跨 Webhook 的普遍先后关系。

正在加载 GitHub 文件…

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

匹配条件决定故障影响面

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

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

实际调用前的匹配入口是 dispatcher 使用的 ShouldCallHook。它返回没有 invocation 时,dispatcher 直接跳过调用。这比单看 Webhook 服务的可用性更能解释“为什么同一时刻有些资源能创建,有些不能”。官方动态准入文档列出了具体匹配字段和适用语义。

Fail 和 Ignore 处理的是调用失败

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

正在加载 GitHub 文件…

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

正在加载 GitHub 文件…

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

更容易误解的是显式拒绝。Webhook 正常返回 allowed: false,表示策略已经做出了拒绝判断,不属于“调用失败”。validating dispatcher 区分 ErrCallingWebhook 与 ErrWebhookRejection:只有前一类进入 Ignore 的放行分支,显式拒绝仍作为错误返回。

正在加载 GitHub 文件…

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。先按该目录 README 的固定提交准备步骤创建独立工作目录,再运行实验。

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

正在加载 GitHub 文件…

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

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

1
2
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 读取有效 Namespace 策略,按级别和版本运行检查。Pod 控制器模板可以获得警告与审计信息,但 enforce 的关键检查落在真实 Pod 请求上,因此 Deployment 对象创建成功,不保证控制器随后创建的 Pod 也能通过。

正在加载 GitHub 文件…

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

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

#Kubernetes #源码分析 #Kind