本文发表已超过一年。较旧的文章可能包含过时内容。请检查页面中的信息自发表以来是否已变得不正确。
Kubernetes 准入控制器指南
Kubernetes 极大地提高了当今生产环境中后端集群的速度和可管理性。凭借其灵活性、可扩展性和易用性,Kubernetes 已成为事实上的容器编排标准。Kubernetes 还提供了一系列功能来保护生产工作负载。最近引入的一项安全功能是一组称为“准入控制器”的插件。必须启用准入控制器才能使用 Kubernetes 的一些更高级的安全功能,例如在整个命名空间强制执行安全配置基线的Pod 安全策略。以下必知的技巧和诀窍将帮助你利用准入控制器充分发挥 Kubernetes 的这些安全能力。
什么是 Kubernetes 准入控制器?
简单来说,Kubernetes 准入控制器是管理和强制集群使用方式的插件。它们可以被视为拦截(已认证的)API 请求并可能更改请求对象或完全拒绝请求的守门人。准入控制过程分为两个阶段:首先执行变更(mutating)阶段,然后执行验证(validating)阶段。因此,准入控制器可以充当变更控制器、验证控制器或两者的组合。例如,LimitRanger 准入控制器可以为 Pod 添加默认资源请求和限制(变更阶段),并验证具有显式设置资源需求的 Pod 不会超过 LimitRange 对象中指定的每个命名空间限制(验证阶段)。

准入控制器阶段
值得注意的是,Kubernetes 的某些操作方面被许多用户认为是内置的,但实际上是由准入控制器控制的。例如,当一个命名空间被删除并随后进入 Terminating
状态时,NamespaceLifecycle
准入控制器阻止在该命名空间中创建任何新对象。
在 Kubernetes 附带的 30 多个准入控制器中,有两个因其近乎无限的灵活性而扮演着特殊角色——ValidatingAdmissionWebhooks
和 MutatingAdmissionWebhooks
,截至 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
。 - 治理:准入控制器允许你强制遵守某些规范,例如使用良好的标签、注解、资源限制或其他设置。一些常见的场景包括
- 在不同对象上强制执行标签验证,以确保为各种对象使用正确的标签,例如为每个对象分配团队或项目,或每个部署都指定 app 标签。
- 自动为对象添加注解,例如为“dev”部署资源归属正确的成本中心。
- 配置管理:准入控制器允许你验证集群中运行的对象的配置,并防止任何明显的错误配置影响你的集群。准入控制器对于检测和修复部署时未带语义标签的镜像(例如通过以下方式)非常有用:
- 自动添加或验证资源限制,
- 确保为 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 时咨询 webhook-demo
命名空间中的 webhook-server
服务,向 /mutate
URL 发送 HTTP POST 请求。为了使此配置生效,必须满足几个先决条件。
Webhook REST API
Kubernetes API 服务器向指定的服务和 URL 路径发送 HTTPS POST 请求,请求体中包含一个 JSON 编码的AdmissionReview
对象(其中 Request
字段已设置)。响应则应是一个 JSON 编码的 AdmissionReview
对象,这次是 Response
字段已设置。
我们的演示仓库包含一个函数,负责处理序列化/反序列化的样板代码,让你能够专注于实现操作 Kubernetes API 对象的逻辑。在我们的示例中,实现准入控制器逻辑的函数称为 applySecurityDefaults
,并且可以在 /mutate
URL 下提供此函数的 HTTPS 服务器如下设置:
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 证书的生成在互联网上有充分的文档说明,我们在此仅引用示例中的shell 脚本。
前面显示的 Webhook 配置包含一个占位符 ${CA_PEM_B64}
。在我们创建此配置之前,需要将此部分替换为 CA 证书的 Base64 编码 PEM 格式。可以使用 openssl base64 -A
命令来完成此操作。
测试 Webhook
部署 Webhook 服务器并进行配置后(可以通过调用仓库中的 ./deploy.sh 脚本完成),现在是时候测试并验证 Webhook 是否确实完成了其工作了。仓库包含三个示例:
- 一个未指定安全上下文的 Pod (
pod-with-defaults
)。我们期望此 Pod 以非 root 用户身份运行,用户 ID 为 1234。 - 一个指定了安全上下文并明确允许以 root 身份运行的 Pod (
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 准入控制器在安全性方面提供了显著优势。深入研究两个功能强大的示例以及随附的可用代码,将帮助您开始利用这些强大的功能。
参考资料
- https://kubernetes.ac.cn/docs/reference/access-authn-authz/admission-controllers/
- https://docs.okd.io/latest/architecture/additional_concepts/dynamic_admission_controllers.html
- https://kubernetes.ac.cn/blog/2018/01/extensible-admission-is-beta/
- https://medium.com/ibm-cloud/diving-into-kubernetes-mutatingadmissionwebhook-6ef3c5695f74
- https://github.com/kubernetes/kubernetes/blob/v1.10.0-beta.1/test/images/webhook/main.go
- https://github.com/istio/istio
- https://www.stackrox.com/post/2019/02/the-runc-vulnerability-a-deep-dive-on-protecting-yourself/