亚马逊AWS官方博客

在 Amazon EKS 通过 Kyverno 实现策略即代码

前言

Amazon Elastic Kubernetes Service (  Amazon EKS  ) 是一项云上的托管 Kubernetes (K8S) 服务,使用该服务您可以轻松地在亚马逊云上部署、管理和扩展容器化的应用程序。

在越来越多用户采用Amazon EKS集群部署运行容器应用时,随着使用的深入,大家会发现,或无意的配置错误,或恶意的容器注入,预期外的更改可能会被应用到集群中,这可能会中断集群的操作或破坏集群的完整性,一个典型的例子就是在集群中创建了不必要的特权容器造成了风险敞口。为了控制Pod安全,Kubernetes 提供了 Pod 安全策略 (PSP) 资源。 PSP 指定一组安全设置,Pod 在集群中创建或更新之前必须满足这些设置。

但是,从 Kubernetes 1.21 版开始,PSP 已被弃用,并在 Kubernetes 1.25 版中被彻底删除。Kubernetes 项目记录 了 PSP 被弃用的原因。 简而言之,PSP 让大多数用户感到困惑。 这种混乱导致了许多错误配置;使集群受到过度限制或过度宽松的设置而不受保护。所有这些问题都促使人们需要一种新的、更加用户友好和确定性的 Pod 安全解决方案。

在新的Kubernetes设计中,PSP 会被 Pod 安全准入 (PSA) 取代,这是一种内置准入控制器,可实现 Pod 安全标准 (PSS) 中概述的安全控制。 PSA 和 PSS 在 Kubernetes 1.23 中都达到了 beta 状态,并在Amazon EKS 1.23版本中默认启用。 除PSA和PSS以外,用户也可以通过开源社区的策略即代码(PaC)解决方案用以替换PSP。

PSS 作为一种原生的安全实践,它提供了一种简单但强大的安全准入控制,用户只需要进行简单的annotation配置即可让这些安全检查生效,防止规定外的资源进入集群。

但PSS也有其局限性,PSS目前仅可以使用内置的3种级别的策略,无法自定义安全策略。企业在使用EKS的过程中,会逐渐形成各种规范,从容器的命名,标签,到资源中各种属性字段配置的最佳实践,以及根据企业合规安全方面要求禁止使用的用法等等,这些规范的会形成一组策略(Policy),用于集群控制资源的创建等方面的行为。

出于对控制的日益增长的需求,已经出现了策略即代码 (PaC) 解决方案来满足这些需求。在 Kubernetes 集群中强制执行行为并限制更改范围是客户面临的共同挑战, 使用策略来应用基于规则的 Kubernetes 资源控制是 的一种动态且公认 管理 Kubernetes 配置的方法。 启用自动化的策略是 Kubernetes 管理上共同关心的问题。PaC 解决方案是启用 Kubernetes 集群并提供规定和自动化控制的最佳实践。

PaC 解决方案为组织提供了通过代码编写策略规则的能力。 通过这种方法,组织可以重用 DevOps 和 GitOps 策略来管理和跨容器集群应用这些策略。

什么是Kyverno

对于 Kubernetes,开源软件 (OSS) 社区中提供了多种 PaC 解决方案,例如OPA(Open Policy Agent),Kyverno 等。其中OPA是较成熟的策略引擎方案,但由于OPA的策略使用一种“Rego“语言,且不同于已知编程语言语法,虽然OPA在除Kubernetes外的多数领域都有着广泛应用,但仍然有一定的学习曲线。

而Kyverno作为一种专用于Kubernetes的策略引擎,其最大的特点是其策略语言完全沿用Kubernetes的manifest YAML 文件的写法,熟悉编写Kubernetes YAML 的开发运维人员可快速上手编写所需的策略。因此,本文将使用Kyverno在Amazon EKS 实施PaC进行探讨。

Kyverno 的原理是通过扩展Kubernetes的准入控制器(Admission Controller),进行动态准入控制,其流程可以简述为在Kubernetes的Mutating准入控制器和Validating准入控制器注入其自身的webhook, 当资源的创建/更新请求进入准入控制器后,Kyverno 的webhook会收到准入控制器的Admission Review对象,然后通过Kyverno的控制器进行对应的处理并将处理结果返回给准入控制器

下面就让我们通过几个例子从实战中了解一下如何通过Kyverno实现策略即代码。

工具安装

eksctl

kubectl

helm

部署EKS测试集群

用户可以通过AWS 控制台界面或CLI工具eksctl 对Amazon EKS进行集群操作。本文下述测试将使用eksctl进行集群的创建/删除,同时基于Kyverno 1.8.1 版本进行安装验证。

1. 首先通过eksctl CLI创建一个用于测试的EKS集群,Kyverno策略会影响资源创建的结果,请务必不要在生产环境中测试。

eksctl create cluster --name eks-kyverno-test

该命令会在aws CLI配置的默认的AWS region中创建一个带2个m5.large节点的EKS集群,API节点访问类型为Public

2. 通过Helm部署Kyverno,在本文中,我们将通过Helm安装Kyverno, 安装Chart version 2.6.1,对应Kyverno version 1.8.1。

helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
helm install kyverno kyverno/kyverno -n kyverno --version 2.6.1 --create-namespace --set replicaCount=1

如果在生产环境部署使用时,应将replicaCount设置至少为3以保证高可用

3. 确认部署情况

正确部署后,Kyverno应有2个service和1个deployment运行在kyverno namespace下

同时观察validatingwebhookconfigurationsmutatingwebhookconfigurations资源,在未应用任何策略前,我们可以看到如下图所示,kyverno的部分webhook已注册成功,随着validate/mutate策略的应用后,其对应的resource webhook也会随之进行注册。

Kyverno 策略模板

Kyverno 的策略模板跟Kubernetes的YAML格式一致,这也是Kyverno的最大的特点,只要熟悉Kubernetes资源YAML的写法就能很快上手编写Kyverno策略。

Kyverno 的YAML结构分为以下几个部分:

– Policy本身的属性,包含一组Rule的集合

– 每个Rule包含一个匹配/排除的资源

– 每个Rule包含一个行为模式(validate/generate/mutate/verify)

我们接下来来看一下这些策略如何编写和应用。

应用Kyverno策略

Kyverno支持多种策略,常见的有验证(validate),变更(mutate),生成(generate)以及更多的使用方式,在本文中将主要对这3种较常见的策略进行解读,其他策略的应用可参考Kyverno官方文档说明。

1.  兼容PSS的策略

Kyverno 提供了兼容PSS的策略模式,可通过PSS类似的方式快速应用预置的安全策略,建议在安装Kyverno后至少需要安装配置baseline的安全模式以保证集群的安全运行。

首先我们通过helm安装Kyverno提供的PSS策略。

helm install kyverno-policies kyverno/kyverno-policies -n kyverno

以上命令会部署默认Baseline组的策略,提供基础限制性的策略,禁止已知的策略提升。默认情况下,这些策略会被设置为Audit模式,在该模式下,不会真正的阻挡资源的创建,而是在Policy Report(Kyverno的一种资源)中报告相关资源。关于Kyverno的Policy Report详见下文描述。我们可以配置不同的helm value来更改安装策略时的参数,例如对上面的命令增加下列参数,就会安装策略为 Restricted 组的策略并设置为 enforce (阻止)模式。更多的helm配置可参考其kyverno policies charts的github页面。以下示例仍以baseline以及audit模式进行演示。

--set podSecurityStandard=restricted --set validationFailureAction=enforce

我们测试在baseline策略下运行特权容器,  会发现集群并不会阻止pod的创建,但是对应validate失败的信息会反馈在Kyverno的Policy Report中。

cat > baseline-pod.yaml << EOF
 apiVersion: v1
 kind: Pod
 metadata:
   name: baseline-pod-1
   namespace: default
 spec:
   containers:
   - name: busybox
     image: busybox
     command: ['sh', '-c', 'sleep 999']
     securityContext:
        privileged: true
EOF
kubectl apply -f baseline-pod.yaml

执行完毕可以看到Pod可以正常创建在default namespace下。

运行kubectl get polr -A,可看到PSS每个Policy对应一个report,在特权容器检查项 disallow-privileged-containers 有一项FAIL。

用 kubectl describe polr/cpol-disallow-privileged-containers 查看对应Policy Report的内容,可以看到其中详细描述了该Pod在disallow-privileged-containers策略检查时失败。

通常在开始使用Kyverno时,我们可以安装上述策略并维持在audit模式以保证对集群无影响,当通过一段时间的运行并检查Policy Report的检查结果后,如果策略运行符合我们的预期,可以将策略配置为enforce模式,让其阻挡不符合要求的资源进入集群。

清理环境

kubectl delete -f baseline-pod.yaml

2. validate(验证)行为

首先我们先部署一个validate行为的示例策略。

cat > cpol-pod-require-labels.yaml << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: pod-require-labels
  annotations:
    policies.kyverno.io/category: Compliance
    policies.kyverno.io/description: Rules to enforce labels on Deployment and Pod resources
spec:
  validationFailureAction: enforce
  rules:
  - name: pod-labels
    match:
      resources:
        kinds:
        - Pod
    validate:
      message: "labels app, owner, env are required"
      pattern:
        metadata:
          labels:
            app: "?*"
            owner: "?*"
            env: "?*"
EOF
kubectl apply -f cpol-pod-require-labels.yaml

上面这个策略定义了:

– 策略模式为validationFailureAction: enforce,当验证失败时,阻止资源在集群中创建

– 策略应用在所有的pod上,pod上的必须有app, owner, env 3个label

将策略应用到集群中后我们可以通过kubectl get cpol来查看策略是否已正确创建

接下来我们尝试创建一个不带任何label的pod到集群中

cat > pod-without-label.yaml << EOF
 apiVersion: v1
 kind: Pod
 metadata:
   name: pod-without-label
   namespace: default
 spec:
   containers:
   - name: busybox
     image: busybox
     command: ['sh', '-c', 'sleep 999']
EOF
kubectl apply -f pod-without-label.yaml

我们可以看见执行apply时,会报错被策略阻挡,并给出了被哪条规则阻挡以及验证出错时的消息(定义在上述Policy中的validate/message字段)

接下来我们新建一个带指定label的pod,再次尝试创建

cat > pod-with-label.yaml << EOF
 apiVersion: v1
 kind: Pod
 metadata:
   name: pod-with-label
   namespace: default
   labels:
     app: test-app
     owner: barry
     env: test
 spec:
   containers:
   - name: busybox
     image: busybox
     command: ['sh', '-c', 'sleep 999']
EOF
kubectl apply -f pod-with-label.yaml

这次我们可以看到pod可以成功创建了,这说明策略已正常生效

validate策略是使用最多的策略,通过配置满足业务和合规上的要求,我们可以在资源创建到集群中之前记录(audit)或阻挡(enforce)其行为。例如,不满足label要求的资源不允许创建,或者指定namespace中的资源的image必须来自某个指定的repo前缀等等。

清理环境以避免上述应用的策略对后续测试产生影响

kubectl delete -f cpol-pod-require-labels.yaml
kubectl delete -f pod-with-label.yaml

3. mutate(变更)行为

首先我们先创建一个mutate行为的示例策略

cat > cpol-set-image-pull.yaml << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: set-image-pull-policy
spec:
  rules:
    - name: set-image-pull-policy
      match:
        any:
        - resources:
            kinds:
            - Pod
      mutate:
        patchStrategicMerge:
          spec:
            containers:
              - (image): "*:latest"
                imagePullPolicy: "IfNotPresent"
EOF
kubectl apply -f cpol-set-image-pull.yaml

这个策略定义了:

– 策略应用在所有pod上

– 匹配image使用“latest“结尾,将其imagePullPolicy修改为IfNotPresent。这里(image) 是一种叫做Conditional Anchor的特殊语法,它的作用时仅当image这个tag匹配它的值中声明的模式时,才会进行后续的动作,否则跳过

我们用一个测试pod来验证策略是否生效,下面这个测试pod的image的格式使用了*:latest 格式,但imagePullPolicy设置为了Never。假如镜像是未被拉取过的,在这个策略下就会因不主动拉取镜像而导致启动失败。我们来看看如何通过mutate行为来避免此类错误。

cat > pod-never-pull-image.yaml << EOF
 apiVersion: v1
 kind: Pod
 metadata:
   name: pod-never-pull-image
   namespace: default
 spec:
   containers:
   - name: busybox
     image: busybox:latest
     imagePullPolicy: Never
     command: ['sh', '-c', 'sleep 999']
EOF
kubectl apply -f pod-never-pull-image.yaml

Pod成功创建后,我们查看一下这个Pod的详细信息,kubectl get pod/pod-never-pull-image -o yaml 可以看到其imagePullPolicy不再是之前声明的Never,而是被策略修改为了IfNotPresent

接下来我们来看看mutate的另一种用法,mutate不仅可以修改资源的配置,也可以增加配置项,例如下面这个示例就实现了一个类似容器注入的功能。

首先我们先编写策略并应用到集群中。

cat > cpol-add-image.yaml << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: strategic-merge-patch
spec:
  rules:
  - name: add-image-policy
    match:
      any:
      - resources:
          kinds:
          - Pod
    mutate:
      patchStrategicMerge:
        metadata:
          labels:
            name: "{{request.object.metadata.name}}"
        spec:
          containers:
            - name: "nginx"
              image: "nginx:latest"
              imagePullPolicy: "Never"
              command:
              - ls
EOF
kubectl apply -f cpol-add-image.yaml

这个策略定义了:

– 策略应用在所有pod上

– 对Pod增加一个叫name的label,其值取自自身的name

– 在Pod中增加一个nginx的容器,由于这里我们将注入的nginx容器的镜像拉取策略设置为Never,如果集群中没有nginx的image的话,这个Pod会运行失败,但并不影响我们验证mutate对Pod变更的结果。

接下来我们用下面的测试Pod来验证一下策略

cat > pod-with-single-image.yaml << EOF
 apiVersion: v1
 kind: Pod
 metadata:
   name: pod-with-single-image
   namespace: default
 spec:
   containers:
   - name: busybox
     image: busybox
     command: ['sh', '-c', 'sleep 999']
EOF
kubectl apply -f pod-with-single-image.yaml

当Pod成功创建后,我们观察一下Pod的详细信息,会发现label和容器都如预期一样添加到原Pod中了

mutate是一种很强大的工具,我们可以将一些常见的集群配置要求或最佳实践通过mutate配置到集群,例如镜像拉取策略,默认label,注入容器等等,将这些工作变成集群的“默认”配置并自动应用到匹配的资源上。对于这类可以用“默认”行为处理的场景,mutate是很好的选择。同时mutating admission发生在validating admission之前的阶段,所以资源是以mutate后再进行validate,并不会因为mutate导致逃逸validate的检查。

但必须强调的是,mutate也是一把双刃剑,因为对原资源进行修改是可能发生副作用的,比如修改了错误的字段,或将同名的镜像错误更新,又或者是更新导致后续validate阶段无法通过等等,都可能影响应用的正常使用,因此,在使用mutate策略时,务必要了解mutate的行为模式以及进行充分测试。

清理环境以避免上述应用的策略对后续测试产生影响

kubectl delete -f cpol-add-image.yaml
kubectl delete -f cpol-set-image-pull.yaml
kubectl delete -f pod-never-pull-image.yaml
kubectl delete -f pod-with-single-image.yaml

4. generate(生成)行为

generate策略与mutate策略不同的地方是,mutate策略是修改准入集群的资源本身,比如创建一个Pod时,mutate的应用范围只会在这个pod本身。而generate策略是根据准入集群的资源,创建出额外的Kubernetes资源。

下面这个示例,我们会看到通过一个generate策略,在创建一个新的namespace时,额外在其中生成一个ConfigMap。

首先我们编写一个generate策略并应用到集群中

cat > cpol-gen-cm.yaml << EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: gen-default-cm
spec:
  rules:
  - name: generate ConfigMap
    match:
      any:
      - resources:
          kinds:
          - Namespace
    exclude:
      any:
      - resources:
          namespaces:
          - kube-system
          - default
          - kube-public
          - kyverno
    generate:
      synchronize: true
      apiVersion: v1
      kind: ConfigMap
      name: test-cm
      namespace: "{{request.object.metadata.name}}"
      data:
        kind: ConfigMap
        metadata:
          labels:
            somekey: somevalue
        data:
          KEY1: "Value1"
          KEY2: "Value2"
EOF
kubectl apply -f cpol-gen-cm.yaml

这个策略定义了:

– 应用在namespace资源上,但排除 kube-system , default , kube-public , kyverno 这几个namesapce

– 创建一个新的叫test-cm的configmap,并置于请求创建的同名namespace中

– 这个ConfigMap带有默认的预设值

接下来我们创建一个叫test-ns-1的namespace,创建后我们可以看到在这个namespace下已经出现了一个test-cm的ConfigMap

查看这个ConfigMap,我们可以看到它的label和预设的值都如预期在策略中定义的一样。

generate策略通常会用于集群环境配置上,例如当新建资源时,创建一些支撑性的资源,或者是创建默认的NetworkPolicy, RoleBinding等访问控制相关资源。

常见的例子如在多租户的EKS集群中,创建租户的namespace时一并创建对象RBAC等访问控制资源。通过将此类行为编写为策略,可减少人工操作的负担和避免遗漏。generate也可以复制已有的资源来产生新的资源,限于篇幅,此处不再展开。

清理环境

通过AWS控制台或eksctl命令删除该测试EKS集群以清理环境,避免产生额外的费用。

eksctl delete cluster --name eks-kyverno-test

总结

通过上面几个简单的例子,我们可以快速了解到Kyverno是什么,能做什么。但限于篇幅,无法对Kyverno所有的功能进行介绍,有兴趣的读者可以参考Kyverno官方文档进行深入阅读。

除此之外,Kyverno也提供了CLI工具,以协助用户在Policy部署到集群前进行验证以避免预期外的效果。

策略即代码解决方案提供了自动防护机制,既可以启用用户又可以防止不需要的行为。 选择正确的策略即代码解决方案并非易事,Kyverno解决方案亦并非唯一选择,组织在做出正确选择时必须从多个因素进行考虑, 根据测试和概念验证的结果选择合适企业自身的策略即代码方案。

无论选择哪种解决方案,策略即代码正在成为 DevOps 和纵深防御策略的基础组成部分。

参考链接:

本篇作者

王冰

亚马逊云科技金融行业解决方案架构师, 主要负责企业客户上云,帮助客户进行云架构设计和技术咨询,专注于容器、数据库等技术方向。