Livesense Advent Calendar 14日目の記事です。
概要
CNCFがホストしているOPA(Open Policy Agent)プロジェクトの一部であるGatekeeperで遊んでみたいと思います。
最初はCIでk8sのマニフェストをテストできるconftestの方を題材にしようと思いましたが、既に日本語記事がいくつか書かれてた(参考に記載)ため、情報が少なかったgatekeeperにしました。
この記事では、以下の危険なコマンド(stagingのリソースを本番にapply)を実行した際にGatekeeperでブロックする方法を模索します。
他の記事や公式サンプルと内容が被らないようにするため、若干強引な課題設定にしています。
$ kubectl apply --context production -k manifests/default/app1/overlays/staging
admission webhook "validation.gatekeeper.sh" denied the request: [denied by deny-deployment] expected: production, actual: staging
$ kubectl apply --context production -k manifests/default/app1/overlays/production
アイディアとして、ステージング環境のリソースにはenv: staging
、本番環境のリソースにはenv: production
をlabel(またはannotation)の値として付与しておき、適用しようとしている環境と異なるリソースのapplyをdenyする方法はどうかと考えました。
これらは、kubebuilder等を使って自前でAdmission Webhooksを実装することも可能ですが、Gatekeeperを使うことにより、rego言語でポリシーを書くだけで、簡単に同様のことが実現できます。
補足
このようなミスを防ぐためには、理想的には本番はプライベートクラスタにし、ローカルからは接続できないようにして踏み台等の専用の環境からのみ接続できるようにするのが良さそうです。
Gatekeeperとは
Open Policy Agent Gatekeeperプロジェクトは、Google、Microsoft、RedHat、Styraによるコラボレーションで、Kubernetes環境におけるポリシの適用とガバナンス強化を支援するように設計されている。Gatekeeper 3.0は、このOPA制約をさらに活用して、ユーザがポリシを宣言し、制約テンプレートを共有して、ポリシ違反のリソースを監査可能にする。
Gatekeeperは、コードではなく、設定によってKubernetesアドミッションコントロールをカスタマイズする方法を提供するために考案された。厳しいリソース制限、グローバルに一意な入力名、すべてのイメージを指定されたリポジトリからのみプルすることなど、すべてのPodに対するコンプライアンス要件に対処することがその目的だ。
画像引用元: https://kubernetes.io/blog/2019/08/06/opa-gatekeeper-policy-and-governance-for-kubernetes/
Gatekeeperを試す
まずは、公式が提供しているサンプルで動作をざっくり確認してみましょう。以下の例ではkindで新規にクラスタを作っていますが、minikubeなど何でも構いません。gatekeeper
というラベルが付与されてないことにより、AdmissionWebhookによってnamespaceの作成がブロックされている様子がわかります。
https://github.com/open-policy-agent/gatekeeper
$ kind create cluster
$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/release-3.1/deploy/gatekeeper.yaml
$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/demo/basic/templates/k8srequiredlabels_template.yaml
$ kubectl create namespace test1
namespace/test created
$ kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/master/demo/basic/constraints/all_ns_must_have_gatekeeper.yaml
$ kubectl create namespace test2
Error from server ([denied by ns-must-have-gk] you must provide labels: {"gatekeeper"}): admission webhook "validation.gatekeeper.sh" denied the request: [denied by ns-must-have-gk] you must provide labels: {"gatekeeper"}
これらはどうやって実現しているのでしょうか。中身を覗いてみましょう。
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
# Schema for the `parameters` field
openAPIV3Schema:
properties:
labels:
type: array
items: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg, "details": {"missing_labels": missing}}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("you must provide labels: %v", [missing])
}
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: ns-must-have-gk
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Namespace"]
parameters:
labels: ["gatekeeper"]
ConstraintTemplateのspec.targets.rego
の部分がrego言語で書かれたポリシーです。
regoには馴染み深くないですが、以下のように推測できます。
- ConstraintTemplateでテンプレートを定義し、labelsというパラメータを外部から受け取っている
- input.reviewの内容(AdmissionReview)とパラメータの中身の差分が1つ以上あったら、違反(violation)として扱っている
- エラー内容はmsgに突っ込んでいる
- K8sRequiredLabelsでlabelsというパラメータにgatekeeperを設定している
- 制限の対象はNamespaceのみ
雰囲気はわかりましたが、やはりregoのことがよくわかってません。ローカルで色々試せるようにしたいですね。
Regoを試す
regoを試すには、何通りか方法がありますが、ここではbrewで入れました。
- brew
- バイナリをインストール
- The Rego Playground
$ brew install opa
$ opa run
> msg := sprintf("you must provide labels: %v", ["gatekeeper"])
Rule 'msg' defined in package repl. Type 'show' to see rules.
> msg
"you must provide labels: gatekeeper"
言語仕様はこちらに記載されています。
https://www.openpolicyagent.org/docs/latest/policy-language/
本題
公式サンプルの動作を確認し、regoを手元で試す手段も用意できました。
ようやく本題に着手しましょう。
kubectl apply --context production -k manifests/app1/overlays/staging
結果的に、このようになりました。
https://github.com/kashiwel/gatekeeper-example
以下は抜粋です。
apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata:
name: config
namespace: gatekeeper-system
spec:
sync:
syncOnly:
- group: ""
version: "v1"
kind: "ConfigMap"
apiVersion: v1
kind: ConfigMap
metadata:
name: gatekeeper-config
namespace: gatekeeper-system
data:
env: production
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: envmatched
spec:
crd:
spec:
names:
kind: EnvMatched
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package envmatched
violation[{"msg": msg}] {
env := input.review.object.metadata.annotations[input.parameters.envName]
config := data.inventory.namespace["gatekeeper-system"]["v1"]["ConfigMap"]["gatekeeper-config"]
config.data.env != env
msg := sprintf("expected: %v, actual: %v", [config.data.env, env])
}
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: EnvMatched
metadata:
name: deny-deployment
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
parameters:
envName: example.com/env
kindで構築したクラスタをproductionと見立てて、stagingのリソースをapplyしてみると想定どおりブロックされました。
$ cat manifests/default/app1/overlays/staging/kustomization.yaml
bases:
- ../../base
patchesStrategicMerge:
- deploy.yaml
commonAnnotations:
example.com/env: staging
$ kubectl apply -k manifests/default/app1/overlays/staging/
"manifests/app1/overlays/staging/": admission webhook "validation.gatekeeper.sh" denied the request: [denied by deny-deployment] expected: production, actual: staging
本番のリソースは問題なくapplyできます。
$ cat manifests/default/app1/overlays/production/kustomization.yaml
bases:
- ../../base
patchesStrategicMerge:
- deploy.yaml
commonAnnotations:
example.com/env: production
$ kubectl apply -k manifests/default/app1/overlays/production/
deployment.apps/echoserver configured
あまり実用的な例ではありませんでしたが、目的を達成できました。
補足
opa.runtime()が空
gatekeeper-config
というConfigMapと、以下の部分は補足しなければなりません。
config := data.inventory.namespace["gatekeeper-system"]["v1"]["ConfigMap"]["gatekeeper-config"]
当初はopa.runtime()
で環境変数を含むランタイム情報を取得できると考え、GatekeeperのdeploymentのENV環境変数にstagingやproductionを設定しておき、比較する方法で考えていました。
$ opa run
> opa.runtime()
{
"commit": "",
"config": {},
"env": {
...
"LC_ALL": "en_US.UTF-8",
"LC_CTYPE": "UTF-8",
"SHELL": "/bin/bash",
"SHLVL": "2",
"STARSHIP_SHELL": "fish",
"TERM": "xterm-256color",
},
"version": "0.25.2"
}
violation[{"msg": msg}] {
opa.runtime().env.ENV != input.review.object.metadata.annotations[input.parameters.envName]
msg := sprintf("expected: %v, actual: %v", [opa.runtime().env.ENV, input.review.object.metadata.annotations[input.parameters.envName]])
}
ただ、Gatekeeperで試してみると、opa.runtime()
が空になっていました。Issueを探してみると、どうやら意図的に読み込んでないようです。
We are not currently populating OPA with the contents of ENV
https://github.com/open-policy-agent/gatekeeper/issues/664#issuecomment-639245663
詰んだか…と思いましたが、どうやらsyncという機能を使えば、クラスタ内のリソースをdata.inventory
として読み出せるようでした。
それが、この部分です。ドキュメントに記載されてますが、metadata.name
はconfig
である必要があります。
apiVersion: config.gatekeeper.sh/v1alpha1
kind: Config
metadata:
name: config
namespace: gatekeeper-system
spec:
sync:
syncOnly:
- group: ""
version: "v1"
kind: "ConfigMap"
この指定によって、以下のConfigMapのenv: production
の値が、data.inventory.namespace["gatekeeper-system"]["v1"]["ConfigMap"]["gatekeeper-config"]
として読み込めるようになりました。
apiVersion: v1
kind: ConfigMap
metadata:
name: gatekeeper-config
namespace: gatekeeper-system
data:
env: production
ただ、正直なところ、このような使い方がセキュリティ的に問題ないかは検証できておりません。本番等で使う場合は、ご注意ください。
デバッグ方法
上記のように、ローカルのopa run
だと値が取得できるものが、gatekeeperだと挙動が変わったりします。
javascriptでいうconsole.log
のように値をprintしたいと思って探したところ、trace
というビルトイン関数が使えそうに見えました。ただ、仕込んでも何故かログに出力されません。
https://www.openpolicyagent.org/docs/latest/policy-reference/#debugging
READMEをみると、denyするようにしてmsgに突っ込むのが楽だよと書かれていました。これで、なんとかデバッグできそうです。
A simple way to view the request object is to use a constraint/template that denies all requests and outputs the request object as its rejection message.
https://github.com/open-policy-agent/gatekeeper#viewing-the-request-object
apiVersion: templates.gatekeeper.sh/v1beta1
kind: ConstraintTemplate
metadata:
name: k8sdenyall
spec:
crd:
spec:
names:
kind: K8sDenyAll
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sdenyall
violation[{"msg": msg}] {
msg := sprintf("REVIEW OBJECT: %v", [input.review])
}
その他のユースケース
今回紹介した例は、公式サンプルや日本語記事に存在しない(と思われる)もの、という縛りで作り出した恣意的なものです。
公式のサンプルに様々なユースケースの実装例がありますので、いくつか紹介します。
https://github.com/open-policy-agent/gatekeeper-library
指定したリポジトリのみ許可
最もイメージしやすい用途かと思います。この例では、リポジトリはopenpolicyagent
のみ許可されているため、imageにopenpolicyagent/opa:0.9.2
は指定できますが、nginx
は指定できません。
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
satisfied := [good | repo = input.parameters.repos[_] ; good = startswith(container.image, repo)]
not any(satisfied)
msg := sprintf("container <%v> has an invalid image repo <%v>, allowed repos are %v", [container.name, container.image, input.parameters.repos])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
satisfied := [good | repo = input.parameters.repos[_] ; good = startswith(container.image, repo)]
not any(satisfied)
msg := sprintf("container <%v> has an invalid image repo <%v>, allowed repos are %v", [container.name, container.image, input.parameters.repos])
}
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: repo-is-openpolicyagent
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- "default"
parameters:
repos:
- "openpolicyagent"
OK
apiVersion: v1
kind: Pod
metadata:
name: opa-allowed
spec:
containers:
- name: opa
image: openpolicyagent/opa:0.9.2
NG
apiVersion: v1
kind: Pod
metadata:
name: nginx-disallowed
spec:
containers:
- name: nginx
image: nginx
ユニークなServiceセレクタのみ許可
以下のように、修正ミス等でServiceリソースのselectorが重複してしまった際に検出できるポリシーが紹介されています。
apiVersion: v1
kind: Service
metadata:
name: test1
spec:
selector:
app: test1
apiVersion: v1
kind: Service
metadata:
name: test2
spec:
selector:
app: test1
まとめ
この記事では、以下のことを書きました。
- gatekeeperのREADMEに記載されているサンプルを試して挙動を確認
- 想定しているlabelの値ではない場合はブロック
- 単純な方法で環境変数から取得できないことがわかったため、syncの機能を使い、ConfigMapから強引に取得
- 公式リポジトリに置かれているその他のユースケースの紹介
OPAの汎用性は非常に高く、非常に大きな可能性を秘めていると考えています。
本番環境の導入を見据えて、キャッチアップを続けたいと思います。
参考
- OPA
-
「誰」が「何」をできるかをOPAのレスポンスで返す
- OPAの可能性を感じることができる素晴らしい記事
- アプリケーションにおける権限設計の課題
-
「誰」が「何」をできるかをOPAのレスポンスで返す
- rego
-
Policy Language
- 公式のregoリファレンス
-
Policy Language
- conftest
-
Conftestを用いたCIでのポリシーチェックの紹介
- メルカリ社におけるconftestの導入事例
-
opa test
コマンドによる、ポリシーのテストについても触れられています
-
Conftest で CI 時に Rego を用いたテストを行う
- Kubernetes完全ガイドでお馴染みの青山さんによるPLAID社にconftestを導入した事例
-
Conftestを用いたCIでのポリシーチェックの紹介
- Gatekeeper
-
EKSにOPA GatekeeperをHelmでインストールしてポリシー強制をしてみる
- この記事執筆時点で、EKSとGatekeeperに触れている唯一の記事かと思います
- ECRレジストリからのみpullを許可する例が記載されています
-
Using Gatekeeper as a drop-in Pod Security Policy replacement in Amazon EKS
- AWSブログの記事です
- PSP(Pod Security Policy)の問題点を挙げ、Gatekeeperで
pod.spec.containers.securityContext.allowPrivilegeEscalation
がtrueのPodをブロックする例が紹介されています
-
EKSにOPA GatekeeperをHelmでインストールしてポリシー強制をしてみる