Open Policy Agent with Kubernetes #1
Open Policy AgentをKubernetesで利用する際の記録。(本稿は以下docを参照しながらdockerで動作確認した時の記録になります)
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/)