Kubernetes v1.36:无法删除的准入策略

如果你曾尝试在一组 Kubernetes 集群上强制执行安全策略,你可能遇到过一个令人沮丧的先有鸡还是先有蛋的问题。 你的准入策略是 API 对象,这意味着它们在有人创建之前不存在,并且任何具有适当权限的人都可以删除它们。 在集群引导期间,总是存在一个窗口,此时你的策略尚未生效,并且无法阻止特权用户删除它们。

Kubernetes v1.36 引入了一个 Alpha 特性来解决这个问题: 基于清单的准入控制。它允许你将准入 Webhook 和基于 CEL 的策略定义为磁盘上的文件, 由 API 服务器在启动时、处理任何请求之前加载。

我们正在填补的空白

如今,大多数 Kubernetes 策略强制执行都是通过 API 进行的。 你创建 ValidatingAdmissionPolicy 或 Webhook 配置作为 API 对象,准入控制器会接收它。 这在稳定状态下工作良好,但存在一些基本限制。

在集群引导期间,API 服务器开始处理请求与你的策略创建并生效之间存在一个间隙。 如果你从备份恢复或从 etcd 故障中恢复,这个间隙可能会很大。

还有一个自我保护问题。准入 Webhook 和策略无法拦截对其自身配置资源的操作。 Kubernetes 跳过对 ValidatingWebhookConfiguration 等类型调用 Webhook,以避免循环依赖。 这意味着具有足够权限的用户可以删除你的关键准入策略,而准入链中没有任何东西可以阻止他们。

我们 —— Kubernetes SIG API Machinery —— 希望有一种方式可以说"这些策略始终启用,到此为止。"

工作原理

你在已经通过 --admission-control-config-file 传递给 API 服务器的 AdmissionConfiguration 文件中添加一个 staticManifestsDir 字段。 将其指向一个目录,将你的策略 YAML 文件放在那里,API 服务器会在开始服务之前加载它们。

apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ValidatingAdmissionPolicy
  configuration:
    apiVersion: apiserver.config.k8s.io/v1
    kind: ValidatingAdmissionPolicyConfiguration
    staticManifestsDir: "/etc/kubernetes/admission/validating-policies/"

清单文件是标准的 Kubernetes 资源定义。唯一的要求是这些清单定义的所有对象必须.static.k8s.io 结尾。 这个保留的后缀可以防止与基于 API 的配置冲突,并在查看指标或审计日志时轻松判断准入决策的来源。

以下是一个完整的示例,拒绝 kube-system 之外的特权容器:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "deny-privileged.static.k8s.io"
  annotations:
    kubernetes.io/description: "Deny launching privileged pods, anywhere this policy is applied"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups: [""]
      apiVersions: ["v1"]
      operations: ["CREATE", "UPDATE"]
      resources: ["pods"]
  variables:
  - name: allContainers
    expression: >-
      object.spec.containers +
      (has(object.spec.initContainers) ? object.spec.initContainers : []) +
      (has(object.spec.ephemeralContainers) ? object.spec.ephemeralContainers : [])
  validations:
  - expression: >-
      !variables.allContainers.exists(c,
      has(c.securityContext) && has(c.securityContext.privileged) &&
      c.securityContext.privileged == true)
    message: "Privileged containers are not allowed"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "deny-privileged-binding.static.k8s.io"
  annotations:
    kubernetes.io/description: "Bind deny-privileged policy to all namespaces except kube-system"
spec:
  policyName: "deny-privileged.static.k8s.io"
  validationActions:
  - Deny
  matchResources:
    namespaceSelector:
      matchExpressions:
      - key: "kubernetes.io/metadata.name"
        operator: NotIn
        values: ["kube-system"]

保护以前无法保护的内容

我们最兴奋的部分是能够拦截对准入配置资源本身的操作。

使用基于 API 的准入,Webhook 和策略永远不会在 ValidatingAdmissionPolicy 或 ValidatingWebhookConfiguration 等类型上调用。 这个限制存在是有充分理由的:如果 Webhook 可以拒绝对其自身配置的更改,你可能会被锁定,无法通过 API 修复它。

基于清单的策略没有这个问题。如果一个错误的策略阻止了它不应该阻止的东西,你可以修复磁盘上的文件,API 服务器会接收到更改。 不存在循环依赖,因为恢复路径不通过 API。

这意味着你可以编写一个基于清单的策略来防止删除关键的基于 API 的准入策略。 对于管理共享集群的平台团队来说,这是一个重大改进。 你现在可以保证你的基线安全策略不会被集群管理员删除,无论是意外还是故意。

以下是实际应用中的示例。此策略防止修改或删除带有 platform.example.com/protected: "true" 标签的准入资源:

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: "protect-policies.static.k8s.io"
  annotations:
    kubernetes.io/description: "Prevent modification or deletion of protected admission resources"
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
    - apiGroups: ["admissionregistration.k8s.io"]
      apiVersions: ["*"]
      operations: ["DELETE", "UPDATE"]
      resources:
      - "validatingadmissionpolicies"
      - "validatingadmissionpolicybindings"
      - "validatingwebhookconfigurations"
      - "mutatingwebhookconfigurations"
  validations:
  - expression: >-
      !has(oldObject.metadata.labels) ||
      !('platform.example.com/protected' in oldObject.metadata.labels) ||
      oldObject.metadata.labels['platform.example.com/protected'] != 'true'
    message: "Protected admission resources cannot be modified or deleted"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: "protect-policies-binding.static.k8s.io"
  annotations:
    kubernetes.io/description: "Bind protect-policies policy to all admission resources"
spec:
  policyName: "protect-policies.static.k8s.io"
  validationActions:
  - Deny

有了这个策略,任何带有 platform.example.com/protected: "true" 标签的基于 API 的准入策略或 webhook 配置都受到保护,不会被篡改。 保护本身存储在磁盘上,无法通过 API 删除。

需要知道的几件事

基于清单的配置故意设计为自包含的。它们不能引用 API 资源,这意味着策略没有 paramKind, 准入 Webhook 没有 Service 引用(而是仅使用 URL), 并且绑定只能引用同一清单集中的策略。这些限制存在是因为配置需要在没有任何集群状态的情况下工作, 包括在 etcd 可用之前的启动阶段。

如果你运行多个 API 服务器实例,每个实例独立加载自己的清单文件。没有内置的跨服务器同步。 这与其他基于文件的 API 服务器配置(如静态加密)的模型相同。 启用此特性后,Kubernetes 会在相关指标上公开配置哈希作为标签,因此你可以检测漂移。

文件在运行时会被监视更改,因此你无需重启 API 服务器即可更新策略。 如果你更新清单文件,API 服务器会验证新配置并原子性地切换到新配置。 如果验证失败,它会保留之前的良好配置并记录错误。 这意味着你可以使用标准配置管理工具(Ansible、Puppet,甚至挂载的 ConfigMap)在整个集群中推出策略更改, 而不会导致 API 服务器停机。

启动时的初始加载更加严格:如果任何清单无效,API 服务器将不会启动。这是有意为之的。 在启动时,快速失败比没有预期策略运行更安全。

尝试一下

要在 Kubernetes v1.36 中尝试此特性:

  1. 为每个 kube-apiserver 启用 ManifestBasedAdmissionControlConfig 特性门控。
  2. 创建一个包含静态清单文件的目录。 如果需要将其挂载到 API 服务器运行的 Pod 中,请进行挂载。只读模式即可。
  3. 在你的 AdmissionConfiguration 中配置 staticManifestsDir,指定目录路径。
  4. 使用 --admission-control-config-file 指向你的 AdmissionConfiguration 文件启动 API 服务器。

完整文档位于 Manifest-Based Admission Control, 你可以关注 KEP-5793 了解持续进展。

我们很想听听你的反馈。请在 Kubernetes Slack 上的 #sig-api-machinery 频道联系我们 (如需邀请,请访问 https://slack.k8s.io/)。

如何参与

如果你有兴趣为这个特性或其他 SIG API Machinery 项目做出贡献,请加入 Kubernetes Slack 上的 #sig-api-machinery。 也欢迎你参加 SIG API Machinery 会议, 每两周周三举行一次。