针对 Kubernetes 工作负载的策略工具
原文:Enforcing policies and governance for Kubernetes workloads
作者:Amit Saha
本文将会讲述使用 conftest 这样的静态工具以及 Gatekeeper 之类的集群内 Operator 为 Kubernetes 工作负载提供策略支持的方法。
本文所讲的策略,指的是在 Kubernetes 中,阻止特定工作负载进行部署的方法。
这种要求通常是出于合规的考虑,有一些最佳实践可以推荐给集群管理员:
- 不要运行特权 Pod。
- 不要用 root 运行 Pod。
- 指定资源限制。
- 不要使用
latest
标签的镜像。 - 限制 Linux capability 的使用。
除去上述安全要求,可能还会有一些应用管理方面的需要,例如:
- 所有工作负载都应该有
project
和app
标签。 - 所有工作负载都应该从特定镜像库获取(例如
my-company.com
)。
最后还有一类需求,防止工作负载之间的冲突,例如多个服务不应使用同样的 Ingress 主机名。
下面会分别讲述集群内外进行策略实施的方法。
不符合策略规定的工作负载将被拒绝部署。
集群外方式是通过对 YAML 文件进行静态检查之后,根据检查结果决定是否放行的。
有多种工具能够完成这一任务。
集群内方式是使用 Validating admission controller,这些控制器会在工作负载进入数据库之前进行调用。
本文所涉的代码可以在 github 找到。
不合规的 Deployment
假设我们有这样一个 YAML:
apiVersion: apps/v1
kind: Deployment
metadata:
name: http-echo
labels:
app: http-echo
spec:
replicas: 2
selector:
matchLabels:
app: http-echo
template:
metadata:
labels:
app: http-echo
spec:
containers:
- name: http-echo
image: hashicorp/http-echo
args: ["-text", "hello-world"]
ports:
- containerPort: 5678
- name: http-echo-1
image: hashicorp/http-echo:latest
args: ["-text", "hello-world"]
ports:
- containerPort: 5678
上面的清单会生成一个 Pod,其中包含两个容器,这两个容器使用的是同一个镜像。
第一个容器没有指定镜像标签,另外一个用的是 latest
,最终他们使用的都是 hashicorp/http-echo
镜像的 latest
版本。
这不符合前面说的最佳实践,应该阻止这种工作负载在我们的集群上运行。正确的指定镜像的方式是填写精确的标签,例如 hashicorp/http-echo:0.2.3
。
那么就看看如何使用静态分析的方式,制定策略来制止这种工作负载的部署。
要阻止这种资源到达集群,可能要在如下位置嵌入这个分析过程:
- Git 的
pre-commit
,在进入 GIT 之前进行检查。 - 作为 CI/CD Pipeline 的一部分,在 Git 分支合并到主线之前进行检查。
- 作为 CI/CD Pipeline 的一部分,在资源被提交到集群之间进行检查。
使用 Conftest 实时策略
Conftest 是一个针对配置文件的测试框架,能够用于对 Kubernetes 清单文件进行检查和校验。
Conftest 的测试使用一种叫 Rego 的 DSL 编写。
可以根据项目网站上的安装指导进行安装。
目前的最新版本为 0.19.0。
接下来定义两条策略:
package main
deny[msg] {
input.kind == "Deployment"
image := input.spec.template.spec.containers[_].image
not count(split(image, ":")) == 2
msg := sprintf("image '%v' doesn't specify a valid tag", [image])
}
deny[msg] {
input.kind == "Deployment"
image := input.spec.template.spec.containers[_].image
endswith(image, "latest")
msg := sprintf("image '%v' uses latest tag", [image])
}
猜猜这两条策略有什么用?
两个策略都是用在 Deployment
对象上的,他们会从 spec.container
字段中获取内容。
第一条规则用于检查镜像是否带有标签:
not count(split(image, ":")) == 2
第二条规则会检查,标签是否为 latest
:
endswith(image, "latest")
如果条件为真,那么 deny
块就会被判为非法。
如果代码中的 deny
超过一个,conftest 会分别进行检查,如果任意一个 deny
生效,都会做出违规判断。
把这段代码保存为 check_image_tag.rego
,并运行 conftest 对 deployment.yaml
进行检查:
$ conftest test -p conftest-checks test-data/deployment.yaml
FAIL - test-data/deployment.yaml - image 'hashicorp/http-echo' doesn't specify a valid tag
FAIL - test-data/deployment.yaml - image 'hashicorp/http-echo:latest' uses latest tag
2 tests, 0 passed, 0 warnings, 2 failures
两个测试的结果都是失败。
conftest
是静态的,需要在把 YAML 提交给集群之前进行检查。
如果已经在使用 CICD 工具向集群提交 YAML,就需要新增一个步骤,使用 conftest 策略对所有资源进行校验。
但是这样就可以阻止用户向集群提交使用 latest
标签的 Deployment
对象吗?
当然了,所有具备权限的人,只要跳过 CICD 就可以用 kubectl apply -f deployment.yaml
中在集群中创建这种违规对象。
所以要在集群中部署动态检查来弥补这种不足——在非法对象被发送给集群之后拒绝。
Kubernetes API
回顾一下创建下面 Pod 的过程:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
spec:
containers:
- name: sise
image: learnk8s/app:1.0.0
ports:
- containerPort: 8080
kubectl apply -f pod.yaml
执行之后,对象定义被发送给 API Server:
- YAML 保存到 ETCD。
- 调度器把 Pod 分配给 Node。
- Kubelet 收到 Pod 定义,并创建对象。
只有这么一点么?
如果 YAML 有拼写错误怎么办?
如何阻止无效的 YAML 进入 ETCD?
在 kubectl apply
时候,首先是 Kubelet 做了一些事:
- 在客户端对资源定义进行检查。
- 把 YAML 转换为 JSON。
- 从
KUBECONFIG
读入配置。 - 把对象报文发送给
kube-apiserver
。
api-server
收到请求之后,也不会立即存入 etcd。
首先他要检查请求者身份是否合法,也就是进行认证。
通过认证之后,还要判断该用户是否有权创建资源?
身份和权限不能混为一谈,能访问集群不代表能够读写所有对象。鉴权过程通常是使用 RBAC 机制实现。
有了 RBAC,就可以通过适当的授权来限制用户的能力了。
假设用户已经能够通过认证并且具备所需权限,是不是就能够把 Pod 定义保存到 Pod 之中了?并不是。
api-server
自身也是一个 Pipeline。
请求报文在保存到数据库之前,还要经过几个组件。认证和授权就是这些组件的一部分,还有其他组件。
在对象进入数据库之前,首先会由 Admission Controller 进行处理。
这个步骤中,就有机会对当前资源进行更多检查。Kubernetes 缺省启用了几个 Admission Controller:
kube-apiserver
的--enable-admission-plugins
中可以看到启用的项目。
下面用 NamespaceLifecycle
为例来看看 Admission Controller 的行为。
Validating admission controllers
NamespaceLifecycle
会阻止用户在不存在的 Namespace 中创建 Pod,例如下面的定义:
apiVersion: v1
kind: Pod
metadata:
name: my-pod
namespace: does-not-exist
spec:
containers:
- name: sise
image: learnk8s/app:1.0.0
ports:
- containerPort: 8080
YAML 结构是有效的,所以它能通过 kubectl
的校验,提交给集群。
假设用户通过了认证和鉴权,这个请求就会进入 NamespaceLifecycle
。does-not-exist
命名空间并不存在,请求被拒绝。
另外 NamespaceLifecycle
还会阻止删除 default
、kube-system
和 kube-public
命名空间的请求。
用于对请求进行校验的控制器被集中在 Validating
分类之中。
除此之外还有另外一个分类,被称为 Mutating
。
Mutating admission controllers
从名字就看得出,Mutating Controller 控制器能够对请求报文做出变更。
DefaultStorageClass
就是一个很好的例子。
假设要创建一个 PVC 对象:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 3Gi
这个对象提交之后,如果使用 kubectl get pvc
,会发现存储卷状态为 Bound
并且被赋予了一个 standard
的 StorageClass
。
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
my-pvc Bound pvc-059f2da2 3Gi RWO standard 3s
很明显我们的 YAML 中并没有这些定义。用 kubectl get pvc my-pvc -0 yaml
查看一下 YAML,会看到如下内容:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: my-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 3Gi
storageClassName: standard
volumeMode: Filesystem
volumeName: pvc-059f2da2-a216-42b7-875e-e7da327605dd
多出了一行 storageClassName: standard
。这里的 standard
并不是 API 中硬编码的,而是把缺省 StorageClass
的名字注入到 spec.storageClassName
之中。
可以用命令读取缺省 StorageClass
:
$ kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE AGE
standard (default) k8s.io/minikube-hostpath Delete Immediate 8m
如果缺省 StorageClass
名称为 aws-ebs
,DefaultStorageClass
Admission Controller 会把它注入到之前 standard
所在的位置。
Kubernetes 带有多个 Mutating 和 Validating Admission Controller,官方网站上有完整的列表。
请求在经过这些关卡之后,才能保存到数据库。
如果想要自定义检查或变更过程,如何能加入自己的规则呢?
Admission Controller 的扩展性
除了 Kubernetes 内置之外,他也可以使用自己的 Admission Controller。
有两个可编程的点:MutationAdmissionWebhook
和 ValidationAdmissionWebhook
。可以在这两个 Webhook 上注册自己的组件,这样在 Admission 阶段就可以使用自定义的组件来处理对象了。
因此可以编写一个组件,来检查当前 Pod 是否使用了来自特定私有镜像库的镜像。
把这个组件注册到 ValidationAdmissionWebhook
,来对容器定义做出放行或阻拦的决策。
这就是 Gatekeeper 的用途——它可以被注册到集群,对请求信息进行校验。
用 Gatekeeper 实施策略
Gatekeeper 让 Kubernetes 管理员可以定义策略来保证集群的合规性,并符合最佳实践的要求。
Gatekeeper 会将自己注册到 Validation Webhook。
提交到集群的任何资源都会被活动的策略进行检查。
同时 Gatekeeper 也符合 Kubernetes 的架构建议,使用 CRD 来管理策略,因此它的策略也是 Kubernetes 资源。
Google 云文档在这方面有很精彩的阐述。
从内部看,Gatekeeper 使用 Open Policy Agent(OPA) 作为核心的策略引擎,策略使用 Rego 语言编写——和 conftest
一样。
在下面的内容里,会尝试使用 Gatekeeper。此处要求用户使用管理用户操作 Kubernetes,使用下面的指令部署 Gatekeeper:
kubectl apply -f \
https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml
检查运行情况:
$ kubectl -n gatekeeper-system describe svc gatekeeper-webhook-service
Name: gatekeeper-webhook-service
Namespace: gatekeeper-system
Labels: gatekeeper.sh/system=yes
Annotations: ...
Type: ClusterIP
IP: 10.102.199.165
Port: <unset> 443/TCP
TargetPort: 8443/TCP
Endpoints: 172.18.0.4:8443
这个服务就是用于验证的的。所有的 Pod、Deployment、Service 等,都要受到 Gatekeeper 的监管
使用 ContstraintTemplate 定义可复用的策略
在 Gatekeeper 中,首先需要使用 ContstraintTemplate
创建策略。
下面的 ContstraintTemplate
定义会拒绝使用 latest
标签的镜像。
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8simagetagvalid
spec:
crd:
spec:
names:
kind: K8sImageTagValid
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8simagetagvalid
violation[{"msg": msg, "details":{}}] {
image := input.review.object.spec.template.spec.containers[_].image
not count(split(image, ":")) == 2
msg := sprintf("image '%v' doesn't specify a valid tag", [image])
}
violation[{"msg": msg, "details":{}}] {
image := input.review.object.spec.template.spec.containers[_].image
endswith(image, "latest")
msg := sprintf("image '%v' uses latest tag", [image])
}
这个策略和前面 conftest
的策略类似,但是也有些区别。
- 输入对象名称为
input.review.object
,而不是input
,这里也无需检查输入对象的kind
。 deny
规则改为violation
。
violation
块的签名是一个包含两个属性的对象。
- 第一个是字符串类型的
msg
。 - 第二个是
details
对象,其中可以包含任意属性。
这两个属性都会用作返回值。
接下来用 kubectl
把这个定义提交到集群。然后就可以使用 describe
命令查询模板情况:
$ kubectl apply -f templates/check_image_tag.yaml
constrainttemplate.templates.gatekeeper.sh/k8simagetagvalid created
$ kubectl describe constrainttemplate.templates.gatekeeper.sh/k8simagetagvalid
Name: k8simagetagvalid
Namespace:
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
{"apiVersion":"templates.gatekeeper.sh/v1beta1","kind":"ConstraintTemplate","metadata":
{"annotations":{},"name":"k8simagetagvalid"},"spec"...
API Version: templates.gatekeeper.sh/v1beta1
Kind: ConstraintTemplate
这个对象并不能直接用于进行校验。它只是一个策略定义,要使用这个策略,还要创建一个 Constraint
。
创建一个 Constraint
Constraint
对象的含义是“在集群中使用这个策略”。
可以把 ConstraintTemplates
当做一本菜谱,其中包含虽然包含几百个菜式,但是菜谱本身是无法食用的。必须选择菜谱并按照菜谱要求提供相应的材料,进行合适的操作,才能烤出蛋糕。
下面举个例子,用前面的 K8sImageTagValid
创建一个 Contraint
:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sImageTagValid
metadata:
name: valid-image-tag
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
这个 Constraint
引用了 ConstraintTemplate
,并用 spec.match
字段规定了适用的资源类型。这里我们要求针对 api
组下的 Deployment
对象进行检查。
这些字段是数组类型的,因此可以指定多个值,把检查范围扩展到 StatefulSet
、DaemonSet
等。
用 kubectl apply
提交这个对象。
测试策略
用带有两个镜像的 Deployment 进行测试:
$ kubectl apply -f deployment.yaml
Error from server ([denied by valid-image-tag] image 'hashicorp/http-echo' doesn't specify a valid tag
[denied by valid-image-tag] image 'hashicorp/http-echo:latest' uses latest tag): error when creating
"test-data/deployment.yaml": admission webhook "validation.gatekeeper.sh" denied the request:
[denied by valid-image-tag] image 'hashicorp/http-echo' doesn't specify a valid tag
[denied by valid-image-tag] image 'hashicorp/http-echo:latest' uses latest tag
Gatekeeper 拒绝了输入内容,可以看出这个过程是不能跳过的。
如果集群中正在运行工作负载,此时实施 Gatekeeper 策略可能会很有难度——这有因为合规问题导致业务中断的风险。
Gatekeeper 还允许使用 dry-run
模式运行 Constraint
:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sImageTagValid
metadata:
name: valid-image-tag
spec:
enforcementAction: dryrun
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
这个模式中,策略不会阻止工作负载的部署,但是会在对象的 Violation
字段中记录违规行为:
$ kubectl describe k8simagetagvalid.constraints.gatekeeper.sh/valid-image-tag
Name: valid-image-tag
Namespace:
Labels: <none>
Annotations: kubectl.kubernetes.io/last-applied-configuration:
....
Total Violations: 2
Violations:
Enforcement Action: dryrun
Kind: Deployment
Message: image 'hashicorp/http-echo' doesn't specify a valid tag
Name: http-echo
Namespace: default
Enforcement Action: dryrun
Kind: Deployment
Message: image 'hashicorp/http-echo:latest' uses latest tag
Name: http-echo
Namespace: default
Events: <none>
在确保所有工作负载都合规之后,就可以移除 dry-run
,正式启用策略了。
标签检查
这个例子会检查 Deployment 对象,要求必须包含 project
和 app
两个标签。
如果用 conftest
:
package main
deny[msg] {
input.kind == "Deployment"
required := {"app", "project"}
provided := {label | input.metadata.labels[label]}
missing := required - provided
count(missing) > 0
msg = sprintf("you must provide labels: %v", [missing])
}
上面代码中:
required
是一个集合,其中包含了app
和project
两个元素。我们希望每个Deployment
都包含这两个标签。provided
会从输入中读取当前对象的标签。- 两个集合相减,得到缺失的标签的集合,赋值给
missing
。 - 用
count()
函数获取missing
集合的元素数量,如果大于零,代表该输入不合规。
测试一下:
$ conftest test -p conftest-checks/check_labels.rego test-data/deployment.yaml
FAIL - test-data/deployment.yaml - you must provide labels: {"project"}
1 test, 0 passed, 0 warnings, 1 failure
在 YAML 中加入要求的标签,才能通过测试。
apiVersion: apps/v1
kind: Deployment
metadata:
name: http-echo
labels:
app: http-echo
project: test
...
前面提到,这样的不合规对象还是可以提交给集群的,因此还是需要 Gatekeeper 来在集群之中使用策略。
首先创建一个 ConstraintTemplate
:
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
# Schema for the `parameters` field
openAPIV3Schema:
properties:
labels:
type: array
items: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg, "details": {"missing_labels": missing}}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("you must provide labels: %v", [missing])
}
上述代码演示了如何从输入中抓取参数。
并且这里使用 openAPIV3Schema
对输入进行过滤,这一节代码表示要求输入对象有一个参数 label
,其数据类型为字符串数组。所有输入都通过 input.parameters
属性传递给 constraint
。
模板提交以后,就可以据此创建 Contstraint
了:
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: deployment-must-have-labels
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
parameters:
labels: ["app", "project"]
这个对象提交以后,集群中就有了校验镜像和校验标签的两个 Constraint
。
再次创建前面的 Deployment:
$ kubectl apply -f deployment.yaml
Error from server ([denied by deployment-must-have-labels] you must provide labels: {"project"}
[denied by valid-image-tag] image 'hashicorp/http-echo' doesn't specify a valid tag
[denied by valid-image-tag] image 'hashicorp/http-echo:latest' uses latest tag): error when creating
"deployment.yaml": admission webhook "validation.gatekeeper.sh" denied the request:
[denied by deployment-must-have-labels] you must provide labels: {"project"}
[denied by valid-image-tag] image 'hashicorp/http-echo' doesn't specify a valid tag
[denied by valid-image-tag] image 'hashicorp/http-echo:latest' uses latest tag
意料之中的创建失败。只有修改 YAML,合规之后才能通过。
总结
Conftest 和 Gatekeeper 都是用 Rego 语言定义策略的,这两个工具结合起来就能覆盖集群内外的校验要求了。
正如你所见,Conftest 的 Rego 策略需要做点修改才能用在 Gatekeeper 里。Konstraint 项目可以解决这个问题。该工具能把 Conftest
策略转换为 Gatekeeper 的 ConstraintTemplate
和 Constraint
,并且它还能方便地对策略进行测试。
除了本文的两个工具之外,集群内外的策略检查都还有别的选择。它们的主要优势就是使用了通用的 Rego 语言。例如 Polaris 同时提供了集群内外的校验功能。然而它使用的是基于 JSON 的策略描述方法,其表达能力远弱于 Rego。