# OPA Gatekeeper 几条入门策略

[Gatekeeper](https://github.com/open-policy-agent/gatekeeper) 是基于 [OPA（Open Policy Agent）](https://github.com/open-policy-agent/opa) 的一个 Kubernetes 策略解决方案。在之前的文章中说过，在 PSP/RBAC 等内置方案之外，在 Kubernetes 中还可以通过策略来实现一些额外的管理、安全方面的限制，本文将会从安装开始，介绍几条实用的小策略。

## 安装篇

安装可以通过 `kubectl` 来进行：

```bash
$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/deploy/gatekeeper.yaml

namespace/gatekeeper-system created
......
gatekeeper-validating-webhook-configuration created
```

或者也可以使用 Helm（目前只支持 Helm 2）：

```bash
helm repo add gatekeeper https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/charts/gatekeeper
helm install gatekeeper/gatekeeper --devel
```

## 策略简介

Gatekeeper 的策略通常是由两个资源对象组成的：Template 和 Constraint。

Template：其定义分为两部分：`crd` 和 `targets`，`crd` 的确是一个 CRD 定义，也就是说生成一个 Template CR 对象，会随之生成一个 CRD；`targets` 则是一组 [`rego`](https://www.openpolicyagent.org/docs/latest/policy-language/) 为主体的代码包——个人表示很反对这种 YAML 中加代码的粗暴行径。

Contsraint：这个对象的定义来自于 Template 生成的 CRD，它负责为模板输出两种内容：其一是对 Kubernetes 资源对象的过滤，其二就是根据 CRD 定义，为 Template 提供输入参数。

## 只允许特定用户名操作特定命名空间

在 `cluster-admin` 成为缺省用户的情况下，我们希望限制特定用户在 Namespace 中的能力，例如下面的规则，会检查用户名前缀是否为命名空间名称：

```yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: ns-user
spec:
  crd:
    spec:
      names:
        kind: ns-user
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package nsuser
        violation[{"msg": msg}] {
          user_name = input.review.userInfo.username
          ns = input.review.object.metadata.namespace
          not startswith(user_name, ns)
          msg = sprintf("User %v is denied.", [user_name])
        }
```

上面的代码有几个需要注意的：

1. `metadata.name` 要和 `spec.crd.spec.names.kind` 一致
    
2. 规则顺序执行，使用 `startswith` 函数判断输入内容里面的用户名和命名空间是否为前缀关系
    
3. 如果一致，则规则停止执行；如果不一致，则输出拒绝信息。
    

声明了 Template 之后，使用 `kubectl apply -f` 提交到集群。

然后创建一个 `constraints`：

```yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: ns-user
metadata:
  name: ns-user
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["ServiceAccount"]
```

这里的 `kind` 字段使用的就是前面模板生成的 CRD（所以 template 和 contsrint 同时创建的话，后者的创建过程可能失败）。在 `match` 字段中，我们限制面向的是 ServiceAccount 对象，接下来测试一下：

```bash
$ kubectl create sa ab
Error from server ([denied by ns-user] User dustise@gmail.com is denied.): admission webhook "validation.gatekeeper.sh" denied the request: [denied by ns-user] User dustise@gmail.com is denied.

$ kubectl create sa sbac --kubeconfig=kubeconfig-defaultsa -n default
serviceaccount/sbac created

$ kubectl create deployment nginx --image=nginx
deployment.apps/nginx created
```

上面可以看到，策略成功发挥作用，使用缺省用户无法创建 `sa`，但是可以创建 `deployment`，换用名为 `defaultsa` 的用户，则能够创建成功。

> 这里如果多做一点测试，会发现 `DELETE` 操作是不受限制的，原因是 Gatekeeper 的 Webhook 配置去掉了对 `DELETE` 的反应，可以 `kubectl edit ValidatingWebhookConfiguration gatekeeper-validating-webhook-configuration` 进行编辑，在 `operations` 字段中加入 `DELETE` 元素。

## 只允许特定镜像前缀

如果在某集群中，我们要求仅适用内网仓库中的镜像，可以使用如下策略：

```yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: imagecheck
spec:
  crd:
    spec:
      names:
        kind: imagecheck
      validation:
        openAPIV3Schema:
          properties:
            prefix:
              type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package image
        violation[{"msg": msg}] {
          containers = input.review.object.spec.template.spec.containers
                some i
                image := containers[i].image
                not startswith(image, input.parameters.prefix)
                msg := sprintf("Image '%v' is not allowed.", [image])
        }
```

相对前面的模板，这个模板复杂了一些：

1. 在 `spec.validation` 字段中加入了一个字符串类型的属性，用这个属性作为参数，定义允许使用的容器前缀，使用 `input.parameters.prefix` 的方式来引用参数。
    
2. 有一行奇怪的代码 `some i`，`some`关键字声明了一个名为 `i` 的变量，规则会使用变量 `i` 对数组进行轮询，查找前缀不符合参数要求的镜像名称。
    

```yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: imagecheck
metadata:
  name: imagecheck
spec:
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds:
        - "Deployment"
        - "DaemonSet"
        - "StatefulSet"
  parameters:
    prefix: "dustise/"
```

`Constraints` 中注明，对 `Deployment` 等三种对象进行校验，要求其镜像前缀为 `dustise/`，下面我们进行一个测试：

```bash
$ kubectl create deployment sleep --image=nginx
Error from server ([denied by imagecheck] Image 'nginx' is not allowed.): admission webhook "validation.gatekeeper.sh" denied the request: [denied by imagecheck] Image 'nginx' is not allowed.

$ kubectl create deployment sleep --image=dustise/sleep
deployment.apps/sleep created
```

Nginx 镜像被禁止，而 dustise/sleep 镜像则成功创建。

## Pod 必须具备资源限制

我们建议所有 Pod 都配置资源限制和请求，便于调度，也能预防系统资源滥用。下面的模板会遍历 Pod 定义，并对资源限制不完整的容器发出警告。

```yaml
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
  name: resource-limit
spec:
  crd:
    spec:
      names:
        kind: resource-limit
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package limit
        resources_defined(x) {
          x.resources; x.resources.limits; x.resources.requests
        }
        violation[{"msg": msg}] {
          ctr_list = input.review.object.spec.template.spec.containers
          some i
          ctr = ctr_list[i]
          not resources_defined(ctr)
          msg = sprintf("%v containers without 'resource' fields", [ctr.name])
        }
```

模板文件中，我们定义了一个函数，分号分割的三个判断构成了逻辑与的关系，缺乏任何一个字段都会导致返回 `false`。

接下来创建类似的 Constraint 对象：

```yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: resource-limit
metadata:
  name: resource-limit
spec:
  match:
    kinds:
      - apiGroups: ["apps"]
        kinds:
        - "Deployment"
        - "DaemonSet"
        - "StatefulSet"
```

再次创建 Deployment，会看到新的拒绝信息：

```bash
$ kubectl create deployment sleep2 --image=dustise/sleep 
Error from server ([denied by resource-limit] sleep containers without 'resource' fields): admission webhook "validation.gatekeeper.sh" denied the request: [denied by resource-limit] sleep containers without 'resource' fields
```

如果创建下列代码所包含的 Deployment 对象，则会成功：

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app: sleep
  name: sleep
spec:
  selector:
    matchLabels:
      app: sleep
  template:
    metadata:
      labels:
        app: sleep
    spec:
      containers:
      - image: dustise/sleep
        imagePullPolicy: Always
        name: sleep
        resources:
          limits:
            cpu: 100m
          requests:
            cpu: 100m
      dnsPolicy: ClusterFirst
```

## 小结

Rego 语法还是有点烦人的，好在官方源码中提供了一些样例和基本用途的代码库可以参考。另外也可以用 Rego Playground 进行在线调试，来编写稍微复杂一点的策略。

> 我还是喜欢 Kyverno..
