本文已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。

Kubernetes 准入控制器指南

Kubernetes 大大提高了当今生产环境中后端集群的速度和可管理性。由于其灵活性、可扩展性和易用性,Kubernetes 已成为容器编排器的事实标准。Kubernetes 还提供了一系列用于保护生产工作负载的功能。最近引入的安全性功能是一组称为“准入控制器”的插件。必须启用准入控制器才能使用 Kubernetes 的一些更高级的安全功能,例如 Pod 安全策略,该策略可在整个命名空间中强制执行安全配置基线。以下必备的技巧和窍门将帮助您利用准入控制器来充分利用 Kubernetes 中的这些安全功能。

什么是 Kubernetes 准入控制器?

简而言之,Kubernetes 准入控制器是管理和强制集群使用方式的插件。它们可以被认为是拦截(经过身份验证的)API 请求的网守,并且可以更改请求对象或完全拒绝请求。准入控制过程有两个阶段:首先执行突变阶段,然后执行验证阶段。因此,准入控制器可以充当突变或验证控制器,或者两者兼而有之。例如,LimitRanger 准入控制器可以使用默认资源请求和限制来扩充 Pod(突变阶段),并验证具有显式设置的资源需求的 Pod 不会超过 LimitRange 对象中指定的每个命名空间的限制(验证阶段)。

Admission Controller Phases

准入控制器阶段

值得注意的是,Kubernetes 操作的某些方面(许多用户会认为是内置的)实际上受准入控制器管理。例如,当命名空间被删除并随后进入 Terminating 状态时,NamespaceLifecycle 准入控制器会阻止在此命名空间中创建任何新对象。

在 Kubernetes 附带的 30 多个准入控制器中,有两个因其几乎无限的灵活性而扮演着特殊的角色 - ValidatingAdmissionWebhooksMutatingAdmissionWebhooks,截至 Kubernetes 1.13,两者都处于 Beta 状态。我们将仔细研究这两个准入控制器,因为它们本身不实现任何策略决策逻辑。相反,相应的操作是从集群内部运行的服务的 REST 端点(webhook)获得的。这种方法将准入控制器逻辑与 Kubernetes API 服务器分离,从而允许用户实现自定义逻辑,以便在 Kubernetes 集群中创建、更新或删除资源时执行。

两种准入控制器 webhook 之间的区别非常不言自明:突变准入 webhook 可能会突变对象,而验证准入 webhook 则不会。但是,即使是突变准入 webhook 也可以拒绝请求,从而以验证方式运行。验证准入 webhook 比突变准入 webhook 有两个主要优点:首先,出于安全原因,可能需要禁用 MutatingAdmissionWebhook 准入控制器(或应用更严格的 RBAC 限制,以限制谁可以创建 MutatingWebhookConfiguration 对象),因为它可能会产生令人困惑甚至危险的副作用。其次,如上一个图表所示,验证准入控制器(以及 webhook)在任何突变准入控制器之后运行。因此,验证 webhook 看到的任何请求对象都是将持久化到 etcd 的最终版本。

通过将标志传递给 Kubernetes API 服务器来配置启用的准入控制器集。请注意,旧的 --admission-control 标志在 1.10 中已被弃用,并替换为 --enable-admission-plugins

--enable-admission-plugins=ValidatingAdmissionWebhook,MutatingAdmissionWebhook

Kubernetes 建议默认启用以下准入控制器。

--enable-admission-plugins=NamespaceLifecycle,LimitRanger,ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,MutatingAdmissionWebhook,ValidatingAdmissionWebhook,Priority,ResourceQuota,PodSecurityPolicy

可以在官方 Kubernetes 参考中找到包含其描述的完整准入控制器列表。此讨论将仅关注基于 webhook 的准入控制器。

为什么我需要准入控制器?

  • 安全性:准入控制器可以通过在整个命名空间或集群中强制执行合理的安全基线来提高安全性。内置的 PodSecurityPolicy 准入控制器可能是最突出的示例;例如,它可以用于禁止容器以 root 身份运行或确保容器的根文件系统始终以只读方式挂载。可以使用自定义的基于 webhook 的准入控制器实现的更多用例包括
  • 允许仅从企业已知的特定注册表拉取映像,同时拒绝未知的映像注册表。
  • 拒绝不符合安全标准的部署。例如,使用 privileged 标志的容器可以绕过许多安全检查。可以通过基于 webhook 的准入控制器来缓解此风险,该控制器可以拒绝此类部署(验证)或覆盖 privileged 标志,将其设置为 false
  • 治理:准入控制器允许您强制遵守某些实践,例如具有良好的标签、注释、资源限制或其他设置。一些常见的场景包括
  • 对不同对象强制执行标签验证,以确保对各种对象使用适当的标签,例如,每个对象都分配给团队或项目,或者每个部署都指定应用程序标签。
  • 自动向对象添加注释,例如为“开发”部署资源分配正确的成本中心。
  • 配置管理:准入控制器允许您验证集群中运行的对象的配置,并防止任何明显的配置错误影响您的集群。准入控制器可用于检测和修复部署了没有语义标签的映像,例如通过
  • 自动添加资源限制或验证资源限制,
  • 确保向 Pod 添加合理的标签,或者
  • 确保生产部署中使用的映像引用未使用 latest 标签或带有 -dev 后缀的标签。

通过这种方式,准入控制器和策略管理有助于确保应用程序在不断变化的控制环境中保持合规。

示例:编写和部署准入控制器 Webhook

为了说明如何利用准入控制器 webhook 来建立自定义安全策略,让我们考虑一个解决 Kubernetes 缺点之一的示例:它的许多默认设置都针对易用性和减少摩擦进行了优化,有时会以牺牲安全性为代价。其中一个设置是默认允许容器以 root 身份运行(并且,如果没有进一步配置且 Dockerfile 中没有 USER 指令,也会这样做)。即使容器在某种程度上与底层主机隔离,以 root 身份运行容器确实会增加部署的风险状况,并且应该避免这样做,这只是众多 安全最佳实践之一。例如,最近暴露的 runC 漏洞CVE-2019-5736)只有在容器以 root 身份运行时才会被利用。

您可以使用自定义的突变准入控制器 webhook 来应用更安全的默认值:除非明确要求,否则我们的 webhook 将确保 Pod 以非 root 用户身份运行(如果没有明确分配,我们将分配用户 ID 1234)。请注意,此设置不会阻止您在集群中部署任何工作负载,包括那些合法需要以 root 身份运行的工作负载。它仅要求您在部署配置中显式启用此风险较高的操作模式,同时为所有其他工作负载默认使用非 root 模式。

完整的代码以及部署说明可以在我们随附的 GitHub 存储库中找到。在这里,我们将重点介绍有关 webhook 如何工作的一些更细微的方面。

突变 Webhook 配置

通过在 Kubernetes 中创建 MutatingWebhookConfiguration 对象来定义突变准入控制器 webhook。在我们的示例中,我们使用以下配置

apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  name: demo-webhook
webhooks:
  - name: webhook-server.webhook-demo.svc
    clientConfig:
      service:
        name: webhook-server
        namespace: webhook-demo
        path: "/mutate"
      caBundle: ${CA_PEM_B64}
    rules:
      - operations: [ "CREATE" ]
        apiGroups: [""]
        apiVersions: ["v1"]
        resources: ["pods"]

此配置定义了一个 webhook webhook-server.webhook-demo.svc,并指示 Kubernetes API 服务器在创建 Pod 时,通过向 /mutate URL 发出 HTTP POST 请求来咨询 webhook-demo 命名空间中的 webhook-server 服务。要使此配置工作,必须满足几个先决条件。

Webhook REST API

Kubernetes API 服务器向指定的服务和 URL 路径发出 HTTPS POST 请求,请求体中包含一个 JSON 编码的 AdmissionReview(其中设置了 Request 字段)。响应应该返回一个 JSON 编码的 AdmissionReview,这次设置了 Response 字段。

我们的演示仓库包含一个函数,该函数负责序列化/反序列化的样板代码,使您可以专注于实现对 Kubernetes API 对象进行操作的逻辑。在我们的示例中,实现准入控制器逻辑的函数名为 applySecurityDefaults,并且可以将一个 HTTPS 服务器设置为在 /mutate URL 下提供此函数,如下所示:

mux := http.NewServeMux()
mux.Handle("/mutate", admitFuncHandler(applySecurityDefaults))
server := &http.Server{
  Addr:    ":8443",
  Handler: mux,
}
log.Fatal(server.ListenAndServeTLS(certPath, keyPath))

请注意,为了使服务器在没有提升权限的情况下运行,我们让 HTTP 服务器监听端口 8443。 Kubernetes 不允许在 Webhook 配置中指定端口;它总是假定 HTTPS 端口为 443。但是,由于无论如何都需要一个服务对象,我们可以轻松地将服务的端口 443 映射到容器上的端口 8443。

apiVersion: v1
kind: Service
metadata:
  name: webhook-server
  namespace: webhook-demo
spec:
  selector:
    app: webhook-server  # specified by the deployment/pod
  ports:
    - port: 443
      targetPort: webhook-api  # name of port 8443 of the container

对象修改逻辑

在变异准入控制器 Webhook 中,变异是通过 JSON 补丁执行的。 虽然 JSON 补丁标准包含许多超出本文讨论范围的复杂性,但我们示例中的 Go 数据结构及其用法应该使用户对 JSON 补丁的工作原理有一个很好的初步了解。

type patchOperation struct {
  Op    string      `json:"op"`
  Path  string      `json:"path"`
  Value interface{} `json:"value,omitempty"`
}

为了将 Pod 的 .spec.securityContext.runAsNonRoot 字段设置为 true,我们构建了以下 patchOperation 对象

patches = append(patches, patchOperation{
  Op:    "add",
  Path:  "/spec/securityContext/runAsNonRoot",
  Value: true,
})

TLS 证书

由于 Webhook 必须通过 HTTPS 提供服务,因此我们需要服务器的适当证书。这些证书可以是自签名(更确切地说:由自签名 CA 签名),但是我们需要 Kubernetes 在与 Webhook 服务器通信时指示相应的 CA 证书。另外,证书的公用名 (CN) 必须与 Kubernetes API 服务器使用的服务器名称匹配,对于内部服务而言,它是 <service-name>.<namespace>.svc,即在我们的例子中是 webhook-server.webhook-demo.svc。由于自签名 TLS 证书的生成在 Internet 上有很好的文档,我们只需参考示例中的相应shell 脚本

先前显示的 Webhook 配置包含一个占位符 ${CA_PEM_B64}。在我们创建此配置之前,我们需要将此部分替换为 CA 的 Base64 编码的 PEM 证书。 openssl base64 -A 命令可用于此目的。

测试 Webhook

部署 Webhook 服务器并进行配置(可以通过从仓库调用 ./deploy.sh 脚本来完成)之后,是时候测试并验证 Webhook 是否确实在工作了。仓库包含三个示例

  • 一个未指定安全上下文的 Pod(pod-with-defaults)。我们希望此 Pod 以非 root 用户身份运行,用户 ID 为 1234。
  • 一个确实指定了安全上下文的 Pod,明确允许它以 root 用户身份运行(pod-with-override)。
  • 一个配置冲突的 Pod,指定它必须以非 root 用户身份运行,但用户 ID 为 0(pod-with-conflict)。为了展示对对象创建请求的拒绝,我们增强了准入控制器逻辑以拒绝这种明显的错误配置。

通过运行 kubectl create -f examples/<name>.yaml 创建这些 Pod 之一。在前两个示例中,您可以通过检查日志来验证 Pod 运行的用户 ID,例如

$ kubectl create -f examples/pod-with-defaults.yaml
$ kubectl logs pod-with-defaults
I am running as user 1234

在第三个示例中,应该使用适当的错误消息拒绝对象创建

$ kubectl create -f examples/pod-with-conflict.yaml
Error from server (InternalError): error when creating "examples/pod-with-conflict.yaml": Internal error occurred: admission webhook "webhook-server.webhook-demo.svc" denied the request: runAsNonRoot specified, but runAsUser set to 0 (the root user)

也可以随意使用您自己的工作负载进行测试。当然,您还可以通过更改 Webhook 的逻辑进行更多实验,并查看更改如何影响对象创建。有关如何进行此类更改的更多信息,请参阅仓库的 readme

总结

Kubernetes 准入控制器为安全性提供了显著的优势。深入研究两个强大的示例(附带可用代码)将帮助您开始利用这些强大的功能。

参考资料