CELで書くアドミッションポリシー
目標
外部のWebhookサーバーなしで、APIサーバーの中でCELによって動作するValidatingAdmissionPolicyを自分で作成し、バインディングで適用範囲を絞ったうえで、従来のValidatingWebhookConfigurationオブジェクトも併せて書き、2つの方式の違いを手で確認します。
なぜ重要なのか
RBACが答えるのは「Podを作成できるか」までです。そのPodがprivilegedかどうか、許可されたレジストリのイメージかどうかは、RBACでは表現できません。このギャップを埋めるのが、アドミッションポリシーです。
従来の方法は、ValidatingWebhookConfigurationで外部サーバーを登録することですが、ここには可用性の落とし穴があります。failurePolicy: Failで登録されたWebhookのサーバーが落ちると、対象リソースの作成がすべて拒否され、デプロイが丸ごと止まります。ポリシーを守ろうとして、クラスターを止めてしまうのです。ValidatingAdmissionPolicyは、APIサーバーの中でCEL式を評価するため、ネットワークのホップと外部プロセスがなくなり、この障害モード自体が減ります。
ポリシーとバインディングが分離されているのも、意図された設計です。ポリシーは「何が違反か」だけを定義し、バインディングが「どこに、どれだけ強く適用するか」を決めます。同じポリシーを、ステージングではWarnで、本番ではDenyで有効にできます。
ステップ
- ネームスペース
cks-admissionを作成し、ラベルcks-policy=enforceを付けてください。 - ValidatingAdmissionPolicy
cks-no-latest-tagを作成してください。spec.matchConstraints.resourceRules[0]は、apiGroupsがapps、apiVersionsがv1、operationsがCREATEとUPDATE、resourcesがdeploymentsです。(APIサーバーは、validationsもauditAnnotationsもないポリシーを拒否します。このステップでは、expression: "true"のような仮の検証式を1つ入れておき、ステップ3で本物の式に置き換えます。) - 同じポリシーに
spec.validations[0]を入れてください。expressionは、Deploymentのすべてのコンテナイメージが:latestで終わっていないかを検査するCELで(object.spec.template.spec.containersとallが含まれている必要があります)、messageは空であってはいけません。 - 同じポリシーの
spec.failurePolicyをFailに設定してください。 - ValidatingAdmissionPolicyBinding
cks-no-latest-tag-bindingを作成してください。policyNameはcks-no-latest-tag、validationActionsはDeny、matchResources.namespaceSelector.matchLabelsはcks-policy: enforceです。 - ValidatingAdmissionPolicy
cks-no-privilegedを作成してください。matchConstraintsはコアグループ(apiGroupsは空文字列)、apiVersionsはv1、operationsはCREATE・UPDATE、resourcesはpodsで、validations[0].expressionにはprivilegedという単語とobject.spec.containersが含まれている必要があり、messageも必要です。 - ValidatingWebhookConfiguration
cks-image-verifyを作成してください。Webhookを1つ(nameはverify.images.cks.local)置き、failurePolicyはIgnore、sideEffectsはNone、admissionReviewVersionsはv1、clientConfig.serviceはネームスペースcks-admissionのServiceimage-verifier、rulesはコアグループv1のpodsに対するCREATEにします。 cks-admissionにDeploymentcheckoutを作成してください。replicasは2、セレクターとPodラベルはapp=checkoutとし、Podテンプレートにラベルapp.kubernetes.io/name=checkoutも入れます。コンテナ名はapp、イメージは@sha256:で固定して:latestは使わず、コンテナのsecurityContextにprivileged: falseとallowPrivilegeEscalation: falseを明示します。
参考
kubectl explain validatingadmissionpolicy.spec.matchConstraints.resourceRulesでフィールドを確認してください。- CELの例:
object.spec.template.spec.containers.all(c, !c.image.endsWith(':latest')) - Pod用の例:
!object.spec.containers.exists(c, has(c.securityContext) && c.securityContext.privileged == true) - よくある間違い1: ポリシーだけを作成して、バインディングを作成しません。バインディングのないポリシーは、どのリクエストも検査しません。
- よくある間違い2: Webhookに
sideEffectsやadmissionReviewVersionsを書き漏らすと、APIサーバーがオブジェクトの作成そのものを拒否します。 - よくある間違い3: Webhookサーバーがない状態で
failurePolicy: Failを使うと、対象リソースをまったく作成できなくなります。
ポリシーを適用するネームスペース
ネームスペースcks-admissionを作成し、ラベルcks-policy=enforceを付けてください。
バインディングのnamespaceSelectorが、このラベルを見て対象を選びます。ラベルの名前と値を正確に合わせてください。
ValidatingAdmissionPolicyの骨組み
ValidatingAdmissionPolicycks-no-latest-tagを作成してください。spec.matchConstraints.resourceRules[0]は、apiGroupsがapps、apiVersionsがv1、operationsがCREATEとUPDATE、resourcesがdeploymentsです。(APIサーバーは、validationsもauditAnnotationsもないポリシーを拒否します。このステップでは、expression: "true"のような仮の検証式を1つ入れておき、ステップ3で本物の式に置き換えます。)
matchConstraints.resourceRulesは、アドミッションWebhookのrulesと同じ形です。apiGroups・apiVersions・operations・resourcesの4つをすべて埋める必要があります。また、APIサーバーはvalidationsもauditAnnotationsもないポリシーは作成そのものを拒否するため、このステップでは仮の検証式を1つ入れておき、次のステップで実際の式に置き換えてください。
CEL式で検証ルールを書く
同じポリシーにspec.validations[0]を入れてください。expressionは、Deploymentのすべてのコンテナイメージが:latestで終わっていないかを検査するCELで(object.spec.template.spec.containersとallが含まれている必要があります)、messageは空であってはいけません。
objectは検査対象のリソースです。Deploymentなら、コンテナはobject.spec.template.spec.containersにあり、CELのall()マクロですべてを検査できます。
failurePolicyを決める
同じポリシーのspec.failurePolicyをFailに設定してください。
式の評価が失敗したときにどうするかのポリシーです。セキュリティポリシーなら、開けておく側ではなく、塞ぐ側です。
バインディングでポリシーを有効にする
ValidatingAdmissionPolicyBindingcks-no-latest-tag-bindingを作成してください。policyNameはcks-no-latest-tag、validationActionsはDeny、matchResources.namespaceSelector.matchLabelsはcks-policy: enforceです。
ポリシーは、バインディングがなければ何もしません。validationActionsに何を入れるかが、拒否なのか警告なのかを分けます。
privileged禁止ポリシーを追加する
ValidatingAdmissionPolicycks-no-privilegedを作成してください。matchConstraintsはコアグループ(apiGroupsは空文字列)、apiVersionsはv1、operationsはCREATE・UPDATE、resourcesはpodsで、validations[0].expressionにはprivilegedという単語とobject.spec.containersが含まれている必要があり、messageも必要です。
対象がPodなので、コンテナのパスがDeploymentとは違います。securityContextがないコンテナもあるので、CELで存在するかどうかを先に確認してください。
ValidatingWebhookConfigurationを書く
ValidatingWebhookConfigurationcks-image-verifyを作成してください。Webhookを1つ(nameはverify.images.cks.local)置き、failurePolicyはIgnore、sideEffectsはNone、admissionReviewVersionsはv1、clientConfig.serviceはネームスペースcks-admissionのServiceimage-verifier、rulesはコアグループv1のpodsに対するCREATEにします。
sideEffectsとadmissionReviewVersionsは必須フィールドなので、抜けると作成が拒否されます。また、Webhookサーバーがない状態なので、失敗ポリシーを誤って選ぶとクラスターが止まります。
総合: ポリシーを通過するデプロイ
cks-admissionにDeploymentcheckoutを作成してください。replicasは2、セレクターとPodラベルはapp=checkoutとし、Podテンプレートにラベルapp.kubernetes.io/name=checkoutも入れます。コンテナ名はapp、イメージは@sha256:で固定して:latestは使わず、コンテナのsecurityContextにprivileged: falseとallowPrivilegeEscalation: falseを明示します。
先に作成した2つのポリシーを、どちらも満たす必要があります。タグではなくダイジェストを使い、コンテナのsecurityContextで権限を下げておいてください。