はじめに
「構築編」では、非接続環境のOpenShiftクラスターにOPA/Gatekeeperを導入する基本的な手順を解説しました。しかし、Gatekeeperは、適切なポリシーを定義して適用することで初めて動作します。
この記事では、実際にポリシーの作成と適用を行い、OpenShiftの運用ルールを自動化する方法を解説します。具体的には、リソースの妥当性を検証するバリデーションと、リソースを自動的に変更するミューテーションという、Gatekeeperの二つの主要な動作について深く掘り下げていきます。
前提条件
- バージョン:Gatekeeper v3.20.0
- Gatekeeperが非接続環境のOpenShiftクラスターに導入済みであること
- 参考:OPA/Gatekeeperで始める安心OpenShift運用:構築編
ポリシーの適用範囲
Gatekeeperのポリシーは、ConstraintTemplateとConstraintsという2つのカスタムリソース(CRD)で構成されます。
- ConstraintTemplate:ポリシーのひな形を定義します。どのようなリソースに、どのような条件でポリシーを適用するかを定義します。これはクラスター全体に適用されるクラスタースコープのリソースです。
- Constraints: ConstraintTemplateで定義されたひな形を使い、実際の適用ルールを定義します。例えば、「k8srequiredlabels」というテンプレートを使って、「すべてのPodにappラベルを必須とする」という具体的なルールを作成します。デフォルトではクラスター内のすべてのリソースにポリシーを適用しますが、特定のNamespaceにのみ適用することも可能です。
ポリシーの適用動作
GatekeeperのConstraintリソースでは、enforcementActionというフィールドを使用して、ポリシーに違反したリソースに対してどのようなアクションを実行するかを制御できます。これにより、単にデプロイを拒否するだけでなく、より柔軟な運用が可能になります。
enforcementActionでは、3つのアクションを選択できます。
- deny (デフォルト):ポリシーに違反したリソースの作成や更新を拒否し、デプロイを停止します。
- dryrun:ポリシー違反を記録するだけで、リソースのデプロイはそのまま許可します。
- warn:ポリシー違反をログに警告として出力しますが、デプロイはブロックしません。
これらのアクションを使い分けることで、ポリシーをより柔軟に運用できます。
dryrunはポリシーのテストで有効です。新しいポリシーを適用する前にdryrunモードで試すことで、既存のリソースや今後のデプロイでどのような違反が発生するかを事前に把握できます。これにより、環境に予期せぬ影響を与えることなく、段階的にポリシーを導入することが可能になります。
warnは段階的なルール適用に役立ちます。例えば、「latestタグの使用は非推奨です」といった警告を出しつつ、Podのデプロイ自体は許可するといった運用が可能です。このようにして、運用を徐々に改善していくことができます。
バリデーションポリシー
リソースがポリシーに適合しているか検証し、違反していればリクエストを拒否します。 これは、ポリシーに違反するリソースがクラスターにデプロイされるのを未然に防ぐ動作です。
有効なシーン
- 特権コンテナやホストのネットワークへのアクセスを許可するPodのデプロイを拒否し、セキュリティリスクを排除します。
- すべてのリソースに特定のラベルやアノテーションを必須とすることで、監査や管理を容易にします。
ここでは、よく使われるバリデーションの具体例として、必須ラベルの強制とルートファイルシステムの書き込み禁止を紹介します。
必須ラベルの強制
以下のポリシーは、stagingネームスペースのPodにenvのラベルを必須とします。
ConstraintTemplateの作成
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: 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])
}
Constraintsの作成
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: must-have-required-labels
spec:
match:
namespaces: ["staging"]
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
labels: [“env”]
ポリシーをゼロから書くのは大変な作業ですが、Gatekeeperには公式に提供されている便利なポリシーライブラリがあります。
公式ウェブサイト: https://open-policy-agent.github.io/gatekeeper-library/website/
このライブラリを活用することで、目的に合ったポリシーが存在する場合はポリシーの作成にかかる時間を大幅に短縮でき、より早く安全なOpenShift運用を始めることができます。
作成したConstraintTemplateとConstraintsのマニフェストファイルを適用します。
$ oc project gatekeeper-system
$ oc apply -f ConstraintTemplate_k8srequiredlabels.yaml
$ oc apply -f Constraints_k8srequiredlabels.yaml
作成したポリシーを確認します。
$ oc get constrainttemplate
NAME AGE
k8srequiredlabels 65s
ポリシー違反が見つかった場合は、TOTAL-VIOLATIONSのカウントが増えていきます。
$ oc get constraints
NAME ENFORCEMENT-ACTION TOTAL-VIOLATIONS
must-have-required-labels deny 0
検証用のネームスペースを作成します。
$ oc create namespace staging
以下の指定したラベルを持たないDeploymentのバリデーションを確認してみます。
apiVersion: apps/v1
kind: Deployment
metadata:
name: gatekeeper-test
namespace: staging
spec:
replicas: 1
selector:
matchLabels:
name: gatekeeper-test
template:
metadata:
name: gatekeeper-test
labels:
name: gatekeeper-test
spec:
nodeSelector:
"kubernetes.io/os": linux
containers:
- name: ubi-container
image: registry.redhat.io/ubi8/ubi:latest
command:
- "/bin/sh"
- "-c"
- "echo 'Pod is running with UBI...' && sleep infinity"
$ oc apply -f Deployment_k8srequiredlabels.yaml
ポリシーの適用スコープはPodのみなので、Deploymentオブジェクト自体は作成されるが、その下位のPodがAdmissionWebhookに拒否されるため、Podは起動できません。Podがデプロイされているか確認してみます。
$ oc get all -n staging
Warning: apps.openshift.io/v1 DeploymentConfig is deprecated in v4.14+, unavailable in v4.10000+
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/gatekeeper-test 0/1 0 0 7m5s
NAME DESIRED CURRENT READY AGE
replicaset.apps/gatekeeper-test-766b7bdb6b 1 0 0 7m5s
デプロイされていなかったので、replicasetのdescribeを確認してみます。
$ oc describe replicaset gatekeeper-test-766b7bdb6b -n staging
…
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedCreate 5m34s (x18 over 11m) replicaset-controller Error creating: admission webhook "validation.gatekeeper.sh" denied the request: [must-have-required-labels] you must provide labels: {"env"}
デプロイが許可されなかったことがわかります。
次に、指定したラベルを付与してデプロイしてみます。
apiVersion: apps/v1
kind: Deployment
metadata:
name: gatekeeper-test
namespace: staging
spec:
replicas: 1
selector:
matchLabels:
name: gatekeeper-test
template:
metadata:
name: gatekeeper-test
labels:
name: gatekeeper-test
env: staging
spec:
nodeSelector:
"kubernetes.io/os": linux
containers:
- name: ubi-container
image: registry.redhat.io/ubi8/ubi:latest
command:
- "/bin/sh"
- "-c"
- "echo 'Pod is running with UBI...' && sleep infinity"
$ oc apply -f Deployment_k8srequiredlabels.yaml
Podがデプロイされているか確認してみます。
$ oc get all -n staging
Warning: apps.openshift.io/v1 DeploymentConfig is deprecated in v4.14+, unavailable in v4.10000+
NAME READY STATUS RESTARTS AGE
pod/gatekeeper-test-6b5f4f8d74-5cngp 1/1 Running 0 3s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/gatekeeper-test 0/1 0 0 7m5s
NAME DESIRED CURRENT READY AGE
replicaset.apps/gatekeeper-test-6b5f4f8d74 1 1 1 3s
replicaset.apps/gatekeeper-test-766b7bdb6b 1 0 0 7m5s
これでデプロイが成功しました!
次の検証のためにConstraints、Deploymentは削除しておきます。
$ oc delete -f Deployment_k8srequiredlabels.yaml
$ oc apply -f Constraints_k8srequiredlabels.yaml
ルートファイルシステムの書き込み禁止
セキュリティ上の理由から、PodのsecurityContext.readOnlyRootFilesystem を true に設定することを強制します。
ConstraintTemplateの作成
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8spspreadonlyrootfilesystem
annotations:
metadata.gatekeeper.sh/title: "Read Only Root Filesystem"
metadata.gatekeeper.sh/version: 1.1.1
description: >-
Requires the use of a read-only root file system by pod containers.
Corresponds to the `readOnlyRootFilesystem` field in a
PodSecurityPolicy. For more information, see
https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems
spec:
crd:
spec:
names:
kind: K8sPSPReadOnlyRootFilesystem
validation:
# Schema for the `parameters` field
openAPIV3Schema:
type: object
description: >-
Requires the use of a read-only root file system by pod containers.
Corresponds to the `readOnlyRootFilesystem` field in a
PodSecurityPolicy. For more information, see
https://kubernetes.io/docs/concepts/policy/pod-security-policy/#volumes-and-file-systems
properties:
exemptImages:
description: >-
Any container that uses an image that matches an entry in this list will be excluded
from enforcement. Prefix-matching can be signified with `*`. For example: `my-image-*`.
It is recommended that users use the fully-qualified Docker image name (e.g. start with a domain name)
in order to avoid unexpectedly exempting images from an untrusted repository.
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
code:
- engine: K8sNativeValidation
source:
variables:
- name: containers
expression: 'has(variables.anyObject.spec.containers) ? variables.anyObject.spec.containers : []'
- name: initContainers
expression: 'has(variables.anyObject.spec.initContainers) ? variables.anyObject.spec.initContainers : []'
- name: ephemeralContainers
expression: 'has(variables.anyObject.spec.ephemeralContainers) ? variables.anyObject.spec.ephemeralContainers : []'
- name: exemptImagePrefixes
expression: |
!has(variables.params.exemptImages) ? [] :
variables.params.exemptImages.filter(image, image.endsWith("*")).map(image, string(image).replace("*", ""))
- name: exemptImageExplicit
expression: |
!has(variables.params.exemptImages) ? [] :
variables.params.exemptImages.filter(image, !image.endsWith("*"))
- name: exemptImages
expression: |
(variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
container.image in variables.exemptImageExplicit ||
variables.exemptImagePrefixes.exists(exemption, string(container.image).startsWith(exemption))).map(container, container.image)
- name: badContainers
expression: |
(variables.containers + variables.initContainers + variables.ephemeralContainers).filter(container,
!(container.image in variables.exemptImages) &&
(!has(container.securityContext) ||
!has(container.securityContext.readOnlyRootFilesystem) ||
container.securityContext.readOnlyRootFilesystem != true)
).map(container, container.name)
validations:
- expression: '(has(request.operation) && request.operation == "UPDATE") || size(variables.badContainers) == 0'
messageExpression: '"only read-only root filesystem container is allowed: " + variables.badContainers.join(", ")'
- engine: Rego
source:
rego: |
package k8spspreadonlyrootfilesystem
import data.lib.exclude_update.is_update
import data.lib.exempt_container.is_exempt
violation[{"msg": msg, "details": {}}] {
# spec.containers.readOnlyRootFilesystem field is immutable.
not is_update(input.review)
c := input_containers[_]
not is_exempt(c)
input_read_only_root_fs(c)
msg := sprintf("only read-only root filesystem container is allowed: %v", )
}
input_read_only_root_fs(c) {
not has_field(c, "securityContext")
}
input_read_only_root_fs(c) {
not c.securityContext.readOnlyRootFilesystem == true
}
input_containers {
c := input.review.object.spec.containers[_]
}
input_containers {
c := input.review.object.spec.initContainers[_]
}
input_containers {
c := input.review.object.spec.ephemeralContainers[_]
}
# has_field returns whether an object has a field
has_field(object, field) = true {
object[field]
}
libs:
- |
package lib.exclude_update
is_update(review) {
review.operation == "UPDATE"
}
- |
package lib.exempt_container
is_exempt(container) {
exempt_images := object.get(object.get(input, "parameters", {}), "exemptImages", [])
img := container.image
exemption := exempt_images[_]
_matches_exemption(img, exemption)
}
_matches_exemption(img, exemption) {
not endswith(exemption, "*")
exemption == img
}
_matches_exemption(img, exemption) {
endswith(exemption, "*")
prefix := trim_suffix(exemption, "*")
startswith(img, prefix)
}
Constraintsの作成
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPReadOnlyRootFilesystem
metadata:
name: psp-readonlyrootfilesystem
spec:
match:
namespaces: ["staging"]
kinds:
- apiGroups: [""]
kinds: ["Pod"]
作成したConstraintTemplateとConstraintsのマニフェストファイルを適用します。
$ oc apply -f ConstraintTemplate_readonlyrootfilesystem.yaml
$ oc apply -f Constraints_readonlyrootfilesystem.yaml
作成したポリシーを確認します。
$ oc get constrainttemplate
NAME AGE
k8spspreadonlyrootfilesystem 19s
$ oc get constraints
NAME ENFORCEMENT-ACTION TOTAL-VIOLATIONS
k8spspreadonlyrootfilesystem.constraints.gatekeeper.sh/psp-readonlyrootfilesystem deny 0
以下のルートファイルシステムを読み取り専用にする設定を入れていないDeploymentのバリデーションを確認してみます。
apiVersion: apps/v1
kind: Deployment
metadata:
name: gatekeeper-test
namespace: staging
spec:
replicas: 1
selector:
matchLabels:
name: gatekeeper-test
template:
metadata:
name: gatekeeper-test
labels:
name: gatekeeper-test
spec:
nodeSelector:
"kubernetes.io/os": linux
containers:
- name: ubi-container
image: registry.redhat.io/ubi8/ubi:latest
command:
- "/bin/sh"
- "-c"
- "echo 'Pod is running with UBI...' && sleep infinity"
$ oc apply -f Deployment_readonlyrootfilesystem.yaml
ポリシーの適用スコープはPodのみなので、Deploymentのデプロイは成功します。
Podがデプロイされているか確認してみます。
$ oc get all -n staging
Warning: apps.openshift.io/v1 DeploymentConfig is deprecated in v4.14+, unavailable in v4.10000+
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/gatekeeper-test 0/1 0 0 5s
NAME DESIRED CURRENT READY AGE
replicaset.apps/gatekeeper-test-58f8db6f8 1 0 0 5s
デプロイされていなかったので、replicasetのdescribeを確認してみます。
$ oc describe replicaset gatekeeper-test-58f8db6f8 -n staging
…
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedCreate 14s (x15 over 96s) replicaset-controller Error creating: admission webhook "validation.gatekeeper.sh" denied the request: [psp-readonlyrootfilesystem] only read-only root filesystem container is allowed: ubi-container
デプロイが許可されなかったことがわかります。
次に、ルートファイルシステムを読み取り専用にする設定を入れてデプロイしてみます。
apiVersion: apps/v1
kind: Deployment
metadata:
name: gatekeeper-test
namespace: staging
spec:
replicas: 1
selector:
matchLabels:
name: gatekeeper-test
template:
metadata:
name: gatekeeper-test
labels:
name: gatekeeper-test
env: staging
spec:
nodeSelector:
"kubernetes.io/os": linux
containers:
- name: ubi-container
image: registry.redhat.io/ubi8/ubi:latest
command:
- "/bin/sh"
- "-c"
- "echo 'Pod is running with UBI...' && sleep infinity"
securityContext:
readOnlyRootFilesystem: true
$ oc apply -f Deployment_readonlyrootfilesystem.yaml
$ oc get all -n staging
Warning: apps.openshift.io/v1 DeploymentConfig is deprecated in v4.14+, unavailable in v4.10000+
NAME READY STATUS RESTARTS AGE
pod/gatekeeper-test-5d6ff97778-nkrns 1/1 Running 0 8s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/gatekeeper-test 1/1 1 1 4m5s
NAME DESIRED CURRENT READY AGE
replicaset.apps/gatekeeper-test-58f8db6f8 0 0 0 4m5s
replicaset.apps/gatekeeper-test-5d6ff97778 1 1 1 8s
これでデプロイが成功しました!
次の検証のためにConstraints、Deploymentは削除しておきます。
$ oc delete -f Deployment_readonlyrootfilesystem.yaml
$ oc delete -f Constraints_readonlyrootfilesystem.yaml
ミューテーションポリシー
リソースのリクエストを、ポリシーに基づいて自動的に変更します。 これは、ユーザーが明示的に設定しなくても、必要な設定を自動で追加する動作です。
ミューテーションは以下の4種のCRDを使用して定義されます。今回は、ssign(メタデータ以外を変更する)を使います。
- AssignMetadata:リソースのmetadataセクション(例:ラベルやアノテーション)を変更する際に使います。例えば、新しいPodがデプロイされる際に、自動でapp: my-appというラベルを追加することができます。
- Assign:metadataセクション以外のリソースの変更を定義します。例えば、コンテナに環境変数を追加したり、リソース制限(limitsやrequests)のデフォルト値を設定したりする場合に利用します。
- ModifySet:リスト形式のデータ(argumentsやvolumesなど)に対して、要素の追加や削除を行う際に使います。例えば、すべてのコンテナに特定のコマンド引数を自動で追加することができます。
- AssignImage:コンテナイメージの文字列を自動で変更する際に使います。これは、イメージのタグを自動的に最新バージョンに更新したり、内部レジストリのドメイン名に書き換えたりする場合に便利です。
有効なシーン:
- リソース管理
- PodにCPUやメモリのリソース制限が設定されていない場合、自動的にデフォルト値を追加し、クラスターのリソース枯渇を防ぎます。
- しかし、この機能には注意が必要です。ミューテーションはマニフェストを自動的に変更するため、Gitリポジトリ(IaC)と実際のクラスター間で差分が発生します。例えば、ArgoCDのようなGitOpsツールでは、この差分が原因で「Out of Sync」と表示され、予期せぬ競合が起こる可能性があります。
- このため、開発環境では利便性を優先し、自動変更を適用し、本番環境では監査や透明性を確保するため、マニフェストに設定を明記するなど、環境に合わせて適切に利用することが重要です。
- 運用効率化
- すべてのPodに特定のサービスメッシュ用のサイドカーコンテナを自動で注入する、といった運用を効率化できます。
- ただし、本番環境におけるサイドカー注入は、Istioをはじめとするサービスメッシュでは専用のAdmission Controllerによって行われることが一般的です。具体的には、Istioの公式ドキュメントにあるように、特定のNamespaceにラベルを付与することで自動注入が有効になり、APIサーバーのAdmission WebhookがPod作成時にサイドカーを挿入します。https://istio.io/latest/docs/setup/additional-setup/sidecar-injection/
- 一方で、GatekeeperのMutation機能によるサイドカー注入は、主にポリシー検証の目的で用いられています。Gatekeeper公式ドキュメントによると、このMutationはIstioのサイドカー注入後のPodに似せたモックPodを作成してポリシーを検証するために利用されるため、本番環境で直接IstioのサイドカーをMutationで注入するケースは想定されていません。
https://open-policy-agent.github.io/gatekeeper/website/docs/expansion/ - したがって、サービスメッシュのサイドカー注入をGatekeeperのMutationで自動化するユースケースは主に検証用途であり、本番運用ではIstioなどの専用Admission Controllerを利用するのが一般的です。
リソース制限の自動追加
以下のYAMLは、Podにリソース制限(limits.cpuとlimits.memory)が設定されていない場合、自動的に値を挿入します。
Mutationsの作成
apiVersion: mutations.gatekeeper.sh/v1
kind: Assign
metadata:
name: pod-resource-limits-default
spec:
applyTo:
- groups: [""]
versions: ["v1"]
kinds: ["Pod"]
match:
scope: Namespaced
namespaces: ["staging"]
location: "spec.containers[name:*].resources"
parameters:
assign:
value:
limits:
cpu: "500m"
memory: "256Mi"
requests:
cpu: "250m"
memory: "128Mi"
作成したMutationsのマニフェストファイルを適用します。
$ oc apply -f Mutations_pod-resource-limits-default.yaml
作成したミューテーションを確認します。
$ oc get assign
NAME AGE
pod-resource-limits-default 33s
以下のリソース制限を設定していないDeploymentのミューテーションを確認してみます。
apiVersion: apps/v1
kind: Deployment
metadata:
name: gatekeeper-test
namespace: staging
spec:
replicas: 1
selector:
matchLabels:
name: gatekeeper-test
template:
metadata:
name: gatekeeper-test
labels:
name: gatekeeper-test
spec:
nodeSelector:
"kubernetes.io/os": linux
containers:
- name: ubi-container
image: registry.redhat.io/ubi8/ubi:latest
command:
- "/bin/sh"
- "-c"
- "echo 'Pod is running with UBI...' && sleep infinity"
$ oc apply -f Deployment_mutation.yaml
作成したPodを確認します。
$ oc get pod -n staging
NAME READY STATUS RESTARTS AGE
gatekeeper-test-58f8db6f8-96ghh 1/1 Running 0 16s
Podにリソース制限が設定されていることを確認します。
$ oc describe pod gatekeeper-test-58f8db6f8-96ghh -n staging
…
Containers:
ubi-container:
…
Limits:
cpu: 500m
memory: 256Mi
Requests:
cpu: 250m
memory: 128Mi
…
このように、マニフェストファイルに記載していなくてもリソース制限が設定されます。
継続的なポリシーチェックについて
Gatekeeperは、Admission Webhookによるリアルタイムなチェックだけでなく、既存のリソースに対する継続的な監査も行います。
Gatekeeperのコントローラーは、定期的にクラスター内のすべてのリソースをスキャンし、既存のリソースがポリシーに適合しているかをチェックします。監査の結果、ポリシー違反が発見された場合、Constraintsリソースのstatusフィールドにその情報が記録されます。以下のコマンドで確認することができます。
$ oc describe <ConstraintTemplateで定義したCRD名> <constrains名>
例:$ oc describe k8srequiredlabels must-have-required-labels
これにより、ポリシーを適用する前にデプロイされたリソースや、ポリシーが変更された後に違反状態になったリソースを特定し、修正することができます。
まとめ
この記事では、OPA/Gatekeeperのバリデーションとミューテーションという二つの主要なポリシー適用動作について解説しました。
- バリデーションは、運用ルールの自動化・強制適用に役立ちます。例えば、デプロイ前にPodに特定のラベルが付いているか、セキュリティ設定が適切かなどを自動でチェックし、違反を未然に防ぎます。これにより、手作業による確認ミスをなくし、クラスターの健全性を保つことができます。
- ミューテーションは、必要な設定を自動で修正・追加し、開発者の負担を軽減に貢献します。例えば、リソース制限が設定されていないPodに自動でデフォルト値を設定したり、アプリケーションのPodに監視用のサイドカーコンテナを自動で注入したりすることができます。
これらのポリシーを導入することで、以下のような利用シーンでの課題を解決できます。
- セキュリティ: ルートファイルシステムの読み取り専用の設定を強制し、コンテナイメージが実行時に書き込まれるのを防ぎます。
- コンプライアンス: すべてのアプリケーションに、セキュリティ監査やコスト管理に必要なラベルを強制します。
- 運用: 開発チームが設定を忘れても、Podに適切なリソース制限が自動で適用されるため、リソース枯渇によるサービス停止を防げます。
参考文献
https://open-policy-agent.github.io/gatekeeper/website/docs/
https://open-policy-agent.github.io/gatekeeper-library/website/
https://istio.io/latest/docs/setup/additional-setup/sidecar-injection/
https://open-policy-agent.github.io/gatekeeper/website/docs/expansion/