👮‍♂️

ConftestでK8s ManifestのPolicy Testをする

2023/01/06に公開

はじめに

Kubernetes Manifestに対して使えるPolicy Testingのツールをいくつか試してみます。
全4回くらいの予定です。

  1. ConftestでK8s ManifestのPolicy Testing(本記事)
  2. OPA GatekeeperでK8s ManifestのPolicy Testing
  3. KonstraintでRegoとConstraintTemplateを管理
  4. GatorでConstraintTemplate/Constraintも含めたPolicy Testing

「やってみた」系の記事になりますので、Regoの書き方やConftestのサブコマンドを網羅するような内容ではありません。そちらが気になるかたは下記のサイトをどうぞ。

https://www.conftest.dev

https://zenn.dev/mizutani/books/d2f1440cfbba94/viewer/intro-overview

ConftestでPolicy Testing

ConftestはKubernetes Manifestなどの構造化されたデータに対するポリシーチェックを行うためのツールです。
ポリシーを記述するにはOpen Policy AgentのRego言語を利用します。

K8s Manifestを作成して色々いじってみます。下記のようなDeploymentを用意しました。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: dog-app-deployment
  labels:
    app.kubernetes.io/name: nginx
    app.kubernetes.io/part-of: dog-application
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80

まずはconftestのドキュメントにあるregoコードを使ってみます。
内容としては

  1. Deploymentの各コンテナはroot起動しない設定になっているか
  2. DeploymentのmatchLabelsにはappが設定されているか

のPolicy Testとなります。

package main

deny[msg] {
  input.kind == "Deployment"
  not input.spec.template.spec.securityContext.runAsNonRoot

  msg := "Containers must not run as root"
}

deny[msg] {
  input.kind == "Deployment"
  not input.spec.selector.matchLabels.app

  msg := "Containers must provide app label for pod selectors"
}

移行、K8s Manifestはapp-manifestディレクトリ、Regoはpolicyディレクトリに配置することとします。

.
├── README.md
├── app-manifest
│   └── dog-app-deployment.yaml
└── policy
    └── main_deployment.rego

conftestコマンド実行時、デフォルトで./policyディレクトリにある.regoを参照します。明示的にディレクトリを指定したい場合は--policyオプションを付けて実行する必要があります。

実行してみましょう。

$ conftest test ./app-manifest/dog-app-deployment.yaml --policy ./policy
FAIL - ./app-manifest/dog-app-deployment.yaml - main - Containers must not run as root

2 tests, 1 passed, 0 warnings, 1 failure, 0 exceptions

2つのRuleのうち1つがpased、もう1つはfailureとなりました。
Deploymentを修正して、runAsNonRootを設定します

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
  labels:
    app.kubernetes.io/name: nginx
    app.kubernetes.io/part-of: dog-application
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.14.2
        ports:
        - containerPort: 80
      securityContext:
        runAsNonRoot: true

もう一度実行してみます。

$ conftest test ./app-manifest/dog-app-deployment_runAsNonRoot.yaml --policy ./policy

2 tests, 2 passed, 0 warnings, 0 failures, 0 exceptions

今度は成功しました。

次はServiceのManifestを作成し、それに対してPolicy Testをやってみましょう。

apiVersion: v1
kind: Service
metadata:
  name: dog-app
spec:
  ports:
  - port: 80
    targetPort: 80
  selector:
    app: nginx

Regoは以下のような形に。
Policy Testの内容は

  1. ServiceのtypeはClusterIPが設定されているか(デフォルトの設定でも良いとする)

のような形にしてみます。

package main

deny[msg] {
  input.kind == "Service"
  not object.get(input.spec, "type", "N_DEFINED") == "N_DEFINED"
  not input.spec.type == "ClusterIP"

  msg := "Services must not LoadBalancer"
}

Deploymentの時と同じようにコマンドを実行します。

$ conftest test ./app-manifest/dog-app-service.yaml --policy ./policy

3 tests, 3 passed, 0 warnings, 0 failures, 0 exceptions

失敗はしませんでしたが、3つのRuleが評価されています。
Service向けに書いたRuleは1つだけなのですが、Deployment向けに書いたRuleも実行されてしまっています。

実はConftestにも名前空間のような機能があり、デフォルトではmainのパッケージにある各種Ruleを実行します。
なので、Deployment用とService用でpackageを別にします。

package deployment

deny[msg] {
  input.kind == "Deployment"
  not input.spec.template.spec.securityContext.runAsNonRoot

  msg := "Containers must not run as root"
}

deny[msg] {
  input.kind == "Deployment"
  not input.spec.selector.matchLabels.app

  msg := "Containers must provide app label for pod selectors"
}
package service

deny[msg] {
  input.kind == "Service"
  not object.get(input.spec, "type", "N_DEFINED") == "N_DEFINED"
  not input.spec.type == "ClusterIP"

  msg := "Services must be ClusterIP"
}

もう一度実行してみます。

$ conftest test ./app-manifest/dog-app-service.yaml --policy ./policy --namespace service

1 test, 1 passed, 0 warnings, 0 failures, 0 exceptions

ちゃんとService向けに書いたRegoのRuleのみ実行されました。

DeploymentとServiceのManifestに対するPolicy Testをとりあえず動かすところまでできました。
次にRule自体が正しいかのテストを書いていきます。

Ruleに対するテストを書く前に、テストがしやすいようRegoのコードを少し変更します。

package deployment

deny_run_as_nonroot_is_not_set[msg] {
  input.kind == "Deployment"
  not input.spec.template.spec.securityContext.runAsNonRoot

  msg := "Containers must not run as root"
}

deny_matchlabels_app_is_not_set[msg] {
  input.kind == "Deployment"
  not input.spec.selector.matchLabels.app

  msg := "Containers must provide app label for pod selectors"
}
package service

deny_service_type_is_not_cluster_ip[msg] {
  input.kind == "Service"
  not object.get(input.spec, "type", "N_DEFINED") == "N_DEFINED"
  not input.spec.type == "ClusterIP"

  msg := "Services must be ClusterIP"
}

Conftestで使うRegoのRuleですが、deny_のようなprefixをつければRuleとして認識されるため、上記のような名前をつけることも可能です。

Ruleのテストのファイル名は慣例的に<任意>_test.regoのような名前にすることが多いようです。テストのルールについてはtest_<任意>のような形でないといけません。

Deploymentに対するRuleは下記の通りなので、そこからテストを書いてみます。

  1. Deploymentの各コンテナはroot起動しない設定になっているか
  2. DeploymentのmatchLabelsにはappが設定されているか
Rule テストの内容
1 runAsNonRoottrue
1 runAsNonRootfalse
1 runAsNonRootが設定されていない
2 matchLabelsappが設定されている
2 matchLabelsappが設定されていない
package deployment

test_run_as_nonroot_is_true {
    not deny_run_as_nonroot_is_not_set["Containers must not run as root"] with input as {
		"kind": "Deployment",
		"spec": {
			"template": {
                "spec": {
                    "securityContext": {
                        "runAsNonRoot": true
                    }
                }
            },
		},
	}
}

test_run_as_nonroot_is_false {
    deny_run_as_nonroot_is_not_set["Containers must not run as root"] with input as {
		"kind": "Deployment",
		"spec": {
			"template": {"spec": {"securityContext": {"runAsNonRoot": false}}},
		},
	}
}

test_without_run_as_nonroot {
    deny_run_as_nonroot_is_not_set["Containers must not run as root"] with input as {
		"kind": "Deployment",
		"spec": {
			"template": {"spec": "none"},
		},
	}
}


test_deployment_with_matchlabels {
    not deny_matchlabels_app_is_not_set["Containers must provide app label for pod selectors"] with input as {
		"kind": "Deployment",
		"spec": {
			"selector": {"matchLabels": {
				"app": "app",
			}},
		},
	}
}

test_deployment_without_matchlabels {
    deny_matchlabels_app_is_not_set["Containers must provide app label for pod selectors"] with input as {
		"kind": "Deployment",
		"spec": {
			"selector": {"matchLabels": {
				"none": "none",
			}},
		},
	}
}

Serviceに対するRuleは下記の通りなので、こちらも同じくテストを書いてみます。

  1. ServiceのtypeはClusterIPが設定されているか(デフォルトの設定でも良いとする)
Rule テストの内容
1 typeCluster IP
1 typeがセットされていない(ClusterIPになる)
1 typeNodePort
1 typeLoadBalancer
package service

test_service_type_is_cluster_ip {
    not deny_service_type_is_not_cluster_ip["Services must be ClusterIP"] with input as {
		"kind": "Service",
		"spec": {
			"type": "ClusterIP" 
		},
	}
}

test_service_type_is_not_set {
    not deny_service_type_is_not_cluster_ip["Services must be ClusterIP"] with input as {
		"kind": "Service",
	}
}

test_service_type_is_nodeport {
    deny_service_type_is_not_cluster_ip["Services must be ClusterIP"] with input as {
		"kind": "Service",
 		"spec": {
			"type": "NodePort" 
		},
	}
}

test_service_type_is_loadbalancer {
    deny_service_type_is_not_cluster_ip["Services must be ClusterIP"] with input as {
		"kind": "Service",
 		"spec": {
			"type": "LoadBalancer" 
		},
	}
}

Rule対するテストを実行してみましょう。

$ conftest verify --policy ./policy 

9 tests, 9 passed, 0 warnings, 0 failures, 0 exceptions, 0 skipped

上手くいきました。
今回は単純なRuleですが、実際の現場では複雑なRuleを記載することもあるためRegoのRule自体のテストはしっかりと書きましょう。

以上、Conftestでできることをざっくりまとめると下記の通りとなります。

  • Regoを使ったPolicy Testing(conftest test
  • Regoに記載したRuleのテスト(conftest verify

他にもサブコマンドはあるのでconftest -hで見てみると良いかと思います。

次回はGatekeeperでKubernetes環境上でのPolicy Testingを試してみます。

Discussion