Open Policy Agent with Kubernetes #1

Open Policy AgentをKubernetesで利用する際の記録。(本稿は以下docを参照しながらdockerで動作確認した時の記録になります)

www.openpolicyagent.org


KubernetesではAdmission ControllerがリソースのCRUD操作についてポリシーを管理しているらしい。
OPAをadmission Controllerとしてデプロイすることで、例えば以下のようなことが可能になる。:

・全てのリソースに対して特定のラベルの付与を強制する。
・特定のリポジトリのイメージのみを利用可能にする。
・全てのpodに対してリソースquotaの設定を強制する。
・作成されるIngressオブジェクトの競合を防ぐ。

Admission Controllerは受け取るオブジェクトをミューテーションさせることができます。
OPAをmutating admission controllerとしてデプロイすることで、例えば以下のようなことが可能になる。:

・podにサイドカーをインジェクトする。
・全てのリソースに特定のアノテーションを付与する
・コンテナイメージを書き換えて特定のイメージリポジトリを利用するようにする
・nodeとpodアフィニティーセレクターをDeploymentに含むようにする

# OPA Gatekeeperとは?
OPA GatekeeperはOPAとKubernetes間のインテグレーションを提供するプロジェクト。
まだbeta。

・ポリシーライブラリをパラメータで記述
・ポリシーライブラリのインスタンス化のためのネイティブKubernetes CRDの生成
・ポリシーライブラリを拡張するためのネイティブKubernetes CRDの生成
・監査機能

[Kubernetesの公式ブログ](https://kubernetes.io/blog/2019/08/06/opa-gatekeeper-policy-and-governance-for-kubernetes/)を参照。
If you want to kick the tires:
See the Installation Instructions in the README.
See the demo/bank and demo/agilebank directories for examples policies and setup scripts.


# Kube-mgmtとOPAを動かす

Kuberentes APIサーバーはリソースのCRUD時にOPAにクエリを投げて問い合わせを行う。
APIサーバーはKubernetesオブジェクトの全ての情報をOPAに対してwebhookで送付する。
OPAはadmission reviewを利用してロードしたポリシーを評価。
以下例は、特定のイメージレジストリの利用を禁止するポリシー。

package kubernetes.admission

deny [reason] {
        some container
        input_containers[container]
        not startswith(container.image, "deny.com/")
        reason := "container image refers to illigal registry !"
}

input_containers[container] {
        container := input.request.object.spec.containers[_]
}

input_containers[container] {
        container := input.request.object.spec.template.spec.containers[_]
}

続いて上ポリシーでOPAを起動(せっかくなのでTLSも確認しておく)

~/S/O/opa ❯❯❯ docker run -it -p 8181:8181 --rm -v `pwd`:/tmp/workspace -w /tmp/workspace openpolicyagent/opa run --tls-cert-file ./crt/server.crt --tls-private-key-file ./crt/server.key --server kubernetes.rego


以下の通りpod.jsonを生成

~/S/O/o/data ❯❯❯ cat pod.json
{
  "kind": "AdmissionReview",
  "apiVersion": "admission.k8s.io/v1beta1",
  "request": {
    "kind": {
      "group": "",
      "version": "v1",
      "kind": "Pod"
    },
    "resource": {
      "group": "",
      "version": "v1",
      "resource": "pods"
    },
    "namespace": "opa-test",
    "operation": "CREATE",
    "userInfo": {
      "username": "system:serviceaccount:kube-system:replicaset-controller",
      "uid": "439dea65-3e4e-4fa8-b5f8-8fdc4bc7cf53",
      "groups": [
        "system:serviceaccounts",
        "system:serviceaccounts:kube-system",
        "system:authenticated"
      ]
    },
    "object": {
      "apiVersion": "v1",
      "kind": "Pod",
      "metadata": {
        "creationTimestamp": "2019-08-13T16:01:54Z",
        "generateName": "nginx-7bb7cd8db5-",
        "labels": {
          "pod-template-hash": "7bb7cd8db5",
          "run": "nginx"
        },
        "name": "nginx-7bb7cd8db5-dbplk",
        "namespace": "opa-test",
        "ownerReferences": [
          {
            "apiVersion": "apps/v1",
            "blockOwnerDeletion": true,
            "controller": true,
            "kind": "ReplicaSet",
            "name": "nginx-7bb7cd8db5",
            "uid": "7b6a307f-d9b4-4b65-a916-5d0b96305e87"
          }
        ],
        "uid": "266d2c8b-e43e-42d9-a19c-690bb6103900"
      },
      "spec": {
        "containers": [
          {
            "image": "nginx",
            "imagePullPolicy": "Always",
            "name": "nginx",
            "resources": {},
            "terminationMessagePath": "/dev/termination-log",
            "terminationMessagePolicy": "File",
            "volumeMounts": [
              {
                "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount",
                "name": "default-token-6h4dn",
                "readOnly": true
              }
            ]
          }
        ],
        "dnsPolicy": "ClusterFirst",
        "enableServiceLinks": true,
        "priority": 0,
        "restartPolicy": "Always",
        "schedulerName": "default-scheduler",
        "securityContext": {},
        "serviceAccount": "default",
        "serviceAccountName": "default",
        "terminationGracePeriodSeconds": 30,
        "tolerations": [
          {
            "effect": "NoExecute",
            "key": "node.kubernetes.io/not-ready",
            "operator": "Exists",
            "tolerationSeconds": 300
          },
          {
            "effect": "NoExecute",
            "key": "node.kubernetes.io/unreachable",
            "operator": "Exists",
            "tolerationSeconds": 300
          }
        ],
        "volumes": [
          {
            "name": "default-token-6h4dn",
            "secret": {
              "secretName": "default-token-6h4dn"
            }
          }
        ]
      },
      "status": {
        "phase": "Pending",
        "qosClass": "BestEffort"
      }
    },
    "oldObject": null
  }
}

例のごとくヒアドキュメントでキーを一つ足す。

~/S/O/o/data ❯❯❯ cat <<EOF > v1-pod-input.json
{
        "input": $(cat pod.json)
}
EOF

このpodの生成リクエストを擬似的に投げてみると、、

~/S/O/o/data ❯❯❯ curl -k -X POST https://localhost:8181/v1/data/kubernetes/admission/deny -d @v1-pod-input.json -H 'Content-Type: application/json'
{"result":["container image refers to illigal registry !"]}%                                                                                                                     
 ~/S/O/o/data ❯❯❯

コンテナイメージのレジストリがillegalだよ(上ではスペル間違ってた。。)と返ってくる。

この場合、全てのコンテナ生成リクエストについて、イメージレジストリチェックが入る。
特定のnsに対してのみレジストリの制限を適用するには、以下。

package kubernetes.admission
deny [reason] {
        some container
        input_containers[container]
        not startswith(container.image, "deny.com/")
        reason := "container image refers to illigal registry !"
}

input_containers[container] {
        input.request.namespace == "opa-test1"
        container := input.request.object.spec.containers[_]
}

上のポリシーで、podのjsonの.request.namespaceの値をopa-test1、opa-testとした時、出力は以下の通りになる。

~/S/O/o/data ❯❯❯ curl -k -X POST https://localhost:8181/v1/data/kubernetes/admission/deny -d @v1-pod-input.json -H 'Content-Type: application/json'
{"result":["container image refers to illigal registry !"]}%    #opa-test1のnamespaceに対して、illegalなイメージレジストリを指定した時
~/S/O/o/data ❯❯❯ curl -k -X POST https://localhost:8181/v1/data/kubernetes/admission/deny -d @v1-pod-input.json -H 'Content-Type: application/json'
{"result":[]}%  #opa-testのnamespaceに対して、illegalなイメージレジストリを指定した時

イメージレジストリの制限はopa-test1のnamespaceに対してのみ適用されることを確認。
api-serverへのリターンとしては、以下の内容を返すようだ。

~/S/O/opa ❯❯❯ cat data/admissionReview.json
{
  "kind": "AdmissionReview",
  "apiVersion": "admission.k8s.io/v1beta1",
  "response": {
    "allowed": false,
    "status": {
      "reason": "container image refers to illegal registry (must be hooli.com)"
    }
  }
}
~/S/O/opa ❯❯❯

APIサーバーは"deny overrides"という機能を実装しており、これは、1つでもadmission controllerがリクエストを拒否した場合、そのリクエストは拒否されるという性質を持つ。
これは、複数admission controllerが存在し、他のどのadmission contorllerがそのリクエストを許可するような実装であったとしても、1つでもadmission controllerがリクエストを拒否した場合>リクエストは拒否される。

また、OPAのポリシーはConfigMapを経由して、kube-mgmtサイドカーコンテナを利用することで動的にロードするとのこと。
このkube-mgmtコンテナを経由することで、他のKuberentesオブジェクトもjson形式で、data直下にロードすることができるようだ。
[リンク](https://www.openpolicyagent.org/docs/latest/kubernetes-introduction/)