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

准入控制器阶段
值得注意的是,Kubernetes操作的某些方面,许多用户可能认为是内置的,实际上是由准入控制器管理的。例如,当命名空间被删除并随后进入Terminating
状态时,NamespaceLifecycle
准入控制器可以阻止在此命名空间中创建任何新对象。
在Kubernetes附带的30多个准入控制器中,有两个控制器因其近乎无限的灵活性而扮演着特殊角色——ValidatingAdmissionWebhooks
和MutatingAdmissionWebhooks
,这两个控制器在Kubernetes 1.13中都处于beta状态。我们将仔细研究这两个准入控制器,因为它们本身不实现任何策略决策逻辑。相反,相应的操作是从集群内运行的服务(一个_webhook_)的REST端点获取的。这种方法将准入控制器逻辑与Kubernetes API服务器解耦,从而允许用户在Kubernetes集群中创建、更新或删除资源时实现自定义逻辑。
这两种准入控制器Webhook之间的区别不言而喻:变更准入Webhook可以更改对象,而验证准入Webhook则不能。但是,即使是变更准入Webhook也可以拒绝请求,从而起到验证作用。验证准入Webhook比变更Webhook有两个主要优势:首先,出于安全原因,禁用MutatingAdmissionWebhook
准入控制器(或对谁可以创建MutatingWebhookConfiguration
对象应用更严格的RBAC限制)可能是可取的,因为它可能产生令人困惑甚至危险的副作用。其次,如上图所示,验证准入控制器(以及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
。 - 治理:准入控制器允许您强制遵守某些做法,例如拥有良好的标签、注解、资源限制或其他设置。一些常见场景包括:
- 对不同对象强制执行标签验证,以确保为各种对象使用正确的标签,例如每个对象都分配给一个团队或项目,或者每个部署都指定一个应用程序标签。
- 自动向对象添加注解,例如为“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配置
变更准入控制器Webhook是通过在Kubernetes中创建MutatingWebhookConfiguration
对象来定义的。在我们的示例中,我们使用以下配置:
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
,可以通过以下方式设置一个在/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)运行。 - 一个指定了安全上下文的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准入控制器为安全性提供了显著优势。深入研究两个强大的示例,并附带可用的代码,将帮助您开始利用这些强大的功能。
参考资料
- 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/