近年来越来越多的企业选择使用容器用于软件构建和发布,随着DockerSwarm的逐渐没落,毫无疑问Kubernetes已经成为事实上的容器编排工具标准。
目前使用Kubernetes已经不是什么大的难题,基本上几个命令几个yaml文件就能轻易地使应用运行在Kubernetes平台上。
比如:
而解决如上问题,最有效的方法就是设置准入门槛,不符合规范的请求直接拒绝,而不是先污染后治理。
Kubernetes一开始设计就支持通过准入控制器(AdmissionControllers)方案设置准入门槛,官方也提供了许多现成的准入控制器供用户选择使用。
但是不同的企业可能安全准则、应用最佳实践标准并不一样,现有的准入控制器可能无法完全满足企业的所有需求,重新自研开发控制器则涉及开发成本高,并且后期也不易于维护扩展。
因此,我认为最有效的办法是能通过统一标准的规则引擎实现合规基线检测和修正,针对不同的规范要求,只需要编写对应规则动态注入,无需大量开发控制器插件,无需重启Kubernetes平台。
本文接下来将介绍基于开源OPA实现Kubernetes资源合规基线检查方案。
虽然本文主要介绍如何使用OPA执行Kubernetes资源合规基线检查,但其原理离不开Kubernetes的准入控制,因此本文首先简单介绍下Kubernetes自带的准入控制机制。
众所周知,我们每次请求KubernetesAPI时都会经过APIServer的三大核心关口,层层把关:
kubeapi_pilelines
同理,Kubernetes可以配置多个准入控制器,只有所有准入控制器都判定通过时,请求方可放行。
另外准入控制器还支持对请求的资源进行修改,比如AlwaysPullImages准入控制器会强制修改Pod的imagePullPolicy为Always。
更多的准入控制器可以参考官方文档UsingAdmissionControllers[1]。当然用户也可以根据自己的需求实现自定义准入控制器。
opa-service
比如最典型的基于角色的权限控制(RBAC)就可以抽象成策略模型:定义各个角色具有哪些权限,通过规则判断用户隶属于哪个角色,从而判定该用户具备哪些权限,比如:
还有一个典型的场景便是安全合规性检测,比如我们定义规则Kubernetes的所有Pod不允许使用latest标签镜像,通过这个规则可以查找出所有包含latest标签的违规Pod。我们还可以把这个规则设置为准入门槛,每次创建Pod前,都会通过我们的规则去做评审(Review),如果包含latest镜像则直接拒绝服务。
而OPA的设计理念则是软件服务本体与策略、策略与决策完全松耦合,换句话说,通过标准统一的策略决策,与检测体完全独立,只要检测对象能够转化为标准输入数据源即可。
官方提供了几个典型的应用场景:
使用OPA,最核心的工作便是编写规则,而OPA使用的是Rego语言进行规则定义,因此不妨先简单介绍下Rego。
关于Rego的详细语法可以参考官方文档policy-language[13],这里只简单介绍一些最简单的语法。
Rego最基本的规则rule模板格式如下:
x=y{expr_1expr_2...}如上所有表达式expr均为AND关系,即当且仅当所有的expr判定结果为true时,x会被置为y。
比如下面这条Rule,当x>y且x>z时,x_is_greatest为true:
x_is_greatest=true{x>yx>z}后面变量赋值=true为默认值,可以省略,因此如下写法的效果是完全一样的:
x_is_greatest{x>yx>z}这有点类似Python的语法:
defaultallow=falseallow{input.user=="admin"}allow{input.user=="Jim"input.method=="GET"}如上定义了两个相同的Ruleallow,当满足任意两个条件之一时,allow为true:
因此有如下判定结果:
allowwithinputas{"user":"admin","method":"POST"}#trueallowwithinputas{"user":"Jim","method":"GET"}#trueallowwithinputas{"user":"Jim","method":"POST"}#false除了OR,Rego也不像大多数编程语言一样具有丰富的分支选择语法(if-else),但是可以通过相同的方式实现if-else逻辑,比如实现f(x)函数:
f(x)="A"{x>=90}f(x)="B"{x>=80;x<90}f(x)="C"{x>=70;x<80}f(x)="D"{x<70}前面介绍的语法还是比较容易理解的,Rego与大多数编程语言在使用方面最大的差别在于集合的操作以及迭代遍历,初次使用会感觉特别奇怪。
比如arr[i],如果i为已定义的变量,则与大多数编程语言语义相同,都是取数组对应索引的值。
但是当i未定义时,则表示遍历数组的索引和值,类似Python的enumerate()函数,在后面的迭代过程中可以引用索引值i以及对应数组的值。
>arr:=["a","b","a","c"]>arr[i]+---+--------+|i|arr[i]|+---+--------+|0|"a"||1|"b"||2|"a"||3|"c"|+---+--------+>i:=2>arr[i]"a"特别地,当变量名为_时,则表示忽略索引值:
>arr:=["a","b","a","c"]>arr[_]+--------+|arr[_]|+--------+|"a"||"b"||"a"||"c"|+--------+因此在Rego中遍历数组的方法如下:
containers:=["int32bit.com/nginx:v1.7","dockerhub.com/mysql:5.7"]container:=containers[_]notstartswith(container,"int32bit.com/")...注:如上空格缩进仅为方便理解,实际编写时不需要缩进。
语义类似Python的如下写法:
containers=["int32bit.com/nginx:v1.7","dockerhub.com/mysql:5.7"]forcontainerincontainers:ifnotcontainer.startswith("int32bit.com/"):...当然新版本的Rego也支持了in语法,如上代码可等效为:
根据这种语法进一步引申,判断元素是否包含在列表中,看起来也会比较奇怪:
>arr:=["a","b","c","b"]>arr[i]=="c"#如果i变量未定义,则会遍历数组,返回所有值为c的索引列表+---+|i|+---+|2|+---+>i:=1>arr[i]=="c"#i已定义为数字1,则判断指定索引1中的值是否为cfalse引入了集合后,派生出一种新的规则rule语法,样例如下:
containers:=["int32bit.com/nginx:v1.7","dockerhub.com/mysql:5.7"]deny_images[c]{container:=containers[_]notstartswith(container,"int32bit.com/")c:=container.image}如上的deny_images是一个列表,语义为遍历containers列表,找出不是以int32bit.com/为前缀的元素,加到deny_images中,等效Python语法:
containers=["int32bit.com/nginx:v1.7","dockerhub.com/mysql:5.7"]deny_images=[]forcontainerincontainers:ifnotcontainer.startswith("int32bit.com/"):c=containerdeny_images.append(c)为什么会有如上奇怪的语法?个人感觉Rego的语法虽然理解起来可能比较绕,但是针对集合操作与查询写起来会比Python更简练些。
与Python一样,Rego也支持集合推导(Comprehension),比如:
arr:=["a","b","c","a","c","d"]s:={i|i=arr[_]}等价于Python:
arr=["a","b","c","a","c","d"]s={iforiinarr}除了以上的语法,Rego集成了非常丰富的内置函数,可以参考Regobuilt-in-functions[14],关于Rego的更多详细语法可以参考官方文档policy-language[15]。
前面介绍了Kubernetes的准入控制以及通用策略规则引擎工具OPA。显然,OPA是非常适合用于实现Kubernetes资源的安全合规基线配置检查的。
针对这个场景,OPA官方已提供了现成的集成方案OPAGatekeeper[16],该方案通过Webhook准入控制器方式集成。
可以认为OPAGatekeeper是一个代理Web服务,KubernetesAPIServer通过Webhook准入控制器将请求体转发给OPAGatekeeper评审,请求Body样例如下:
{"kind":"AdmissionReview","request":{"kind":{"kind":"Pod","version":"v1"},"object":{"metadata":{"name":"myapp"},"spec":{"containers":[{"image":"nginx","name":"nginx-frontend"},{"image":"mysql","name":"mysql-backend"}]}}}}OPAGatekeeper基于Repo规则对请求的Body进行判定,并返回给APIServer。
kubernetesadmissionflow
关于OPAGatekeeper的安装配置可以参考官方文档OPAgatekeeperinstall[17],如果只需要默认配置,简单只需要一条apply命令即可:
以一个简单的场景为例,假设我们要求所有的资源必须包含指定的labels,则可定义模板如下:
kind:ConstraintTemplatemetadata:name:k8srequiredlabelsspec:crd:spec:names:kind:K8sRequiredLabelsvalidation:openAPIV3Schema:properties:labels:type:arrayitems:stringtargets:-target:admission.k8s.gatekeeper.shrego:|packagek8srequiredlabelsviolation[{"msg":msg,"details":{"missing_labels":missing}}]{provided:={label|input.review.object.metadata.labels[label]}required:={label|label:=input.parameters.labels[_]}missing:=required-providedcount(missing)>0msg:=sprintf("youmustprovidelabels:%v",[missing])}constrainttemplates模板定义包含两部分:
前面的Rego例子不难理解,首先取出资源Object的labels列表,然后取出要求必须配置的labels列表,最后二者取差集就是缺失的labels列表。
配置了模板后,需要实例化constraint,引用前面的constrainttemplate并配置输入参数:
apiVersion:constraints.gatekeeper.sh/v1beta1kind:K8sRequiredLabelsmetadata:name:namespace-labels-checkspec:match:kinds:-apiGroups:[""]kinds:["Namespace"]parameters:labels:["app_name","org_name"]constraint定义包含两个主要部分:
创建如上资源后,我们尝试创建没有任何label的namespace:
#kubectlcreatensaaaErrorfromserver:youmustprovidelabels:{"app_name","org_name"}admissionwebhook"validation.gatekeeper.sh"deniedtherequest符合预期创建namespace失败,错误中提示没有配置app_name以及org_name标签。
前面我们设置了K8sRequiredLabelsconstraint,当我们创建没有打app_name以及org_name标签的不合规的namespace时会直接拒绝请求。
但是往往在创建K8sRequiredLabelsconstraint之前已经有很多存量的namespaces了,如何查看哪些存量namespaces不合规呢?
OPAGatekeeper提供了如下三种方式:
如下,我们可以查看所有不满足namespace-labels-check的违规namespaces列表:
当我们的规则限制作用于Pod时,则无法直接拒绝创建违规的Deployments或者DaemonSets,但创建的Deployment会创建Pod失败。
比如我们创建如下的constraint实例,要求所有的Pod必须包含app_name以及org_name标签。
apiVersion:constraints.gatekeeper.sh/v1beta1kind:K8sRequiredLabelsmetadata:name:pod-labels-checkspec:match:kinds:-apiGroups:[""]kinds:["Pod"]parameters:labels:["app_name","org_name"]我们创建nginx-appDeployment:
apiVersion:apps/v1kind:Deploymentmetadata:labels:app:nginx-appname:nginx-appspec:replicas:3selector:matchLabels:app:nginx-apptemplate:metadata:labels:app:nginx-appspec:containers:-image:nginx:latestname:nginx此时Deployment创建成功,但是Pod不会被创建出来:
#kubectlgetdeployments.appsNAMEREADYUP-TO-DATEAVAILABLEAGEnginx-app0/3003m49s我们可以查看最近的Event:
#kubectlgeteventsLASTSEENTYPEREASONOBJECTMESSAGE2m31sWarningFailedCreatereplicaset/nginx-app-78b5f9b5fdErrorcreating:admission"pod-label-check"deniedtherequest:youmustprovidelabels:{"app_name","org_name"}可见是由于pod-label-check阻断了Pod资源创建。当然我们也可以通过status获取阻断原因:
status:conditions:-lastTransitionTime:"2021-12-30T10:15:32Z"lastUpdateTime:"2021-12-30T10:15:32Z"message:'admissionwebhook"validation.gatekeeper.sh"deniedtherequest:[pod-labels-check]youmustprovidelabels:{"app_name","org_name"}'reason:FailedCreatestatus:"True"type:ReplicaFailure5.5违规处理前面当我们创建不合规的资源时会直接拒绝请求,这是因为OPA的默认违规处理方式是deny,即拒绝请求。
除了deny阻断模式,OPAGatekeeper还支持如下两种违规处理策略:
以warn为例,我们创建一个违规Pod:
#kubectlapply-fpod.yamlWarning:[prod-repo-is-openpolicyagent]container
Gatekeeper除了支持基于规则执行违规资源检测,还支持对违规资源进行自动修复。不过目前该功能还不是很完善,只支持静态赋值,不支持动态配置。
比如我们要求除kube-systemnamespace以外的所有Pod的imagePullPolicy必须为Always,则可以创建如下Assign实例:
apiVersion:mutations.gatekeeper.sh/v1beta1kind:Assignmetadata:name:set-image-pull-policy-to-alwaysspec:applyTo:-groups:[""]kinds:["Pod"]versions:["v1"]match:scope:Namespacedkinds:-apiGroups:["*"]kinds:["Pod"]excludedNamespaces:["kube-system"]location:"spec.containers[name:*].imagePullPolicy"parameters:assign:value:Always另外一个场景是不允许用户设置Pod的securityContext为privileged,则可以通过自动设置securityContext.privileged为false实现。
apiVersion:mutations.gatekeeper.sh/v1beta1kind:Assignmetadata:name:demo-privilegedspec:applyTo:-groups:[""]kinds:["Pod"]versions:["v1"]match:scope:Namespacedkinds:-apiGroups:["*"]kinds:["Pod"]namespaces:["bar"]location:"spec.containers[name:foo].securityContext.privileged"parameters:assign:value:false5.7开源的模板仓库在Github中官方维护了一些很有用的现成模板gatekeeper-library[18],内容还是很全的,涵盖安全、配置基线等最佳实践规则。
本文首先简单介绍了Kubernetes的资源治理问题以及准入控制器,然后介绍了OPA以及Rego,最后介绍如何将OPA集成到Kubernetes的准入控制器中实现Kubernetes资源的安全合规自动检测。