Certified Kubernetes Security Specialist (CKS) #16 Admission control: OPA/Gatekeeper、Kyverno

#15 イメージ署名: cosign、SBOM では、イメージに署名を付けてその署名を検証する流れを身につけました。ところが署名されていないイメージ、latest タグを使った Pod、root で動くコンテナをクラスターが受け入れる前に防ぐには、リクエストを横取りして検査する関門 が必要です。今回の記事はその関門である admission control を OPA/Gatekeeper と Kyverno の 2 つのツールで扱います。

CKS カリキュラムで admission control は Monitoring・Logging・Runtime Security ドメインにマッピングされますが、Falco がランタイムで起きたことを検知するツールだとすれば、admission control は 危険なワークロードがそもそもクラスターに入れないように防ぐ ポリシー強制ツールです。検知ではなく予防という点で性格が異なります。

admission controller とは何か #

Kubernetes API サーバーは、入ってきたリクエストを処理するときに複数の段階を経ます。認証 (authentication) で誰なのかを確認し、認可 (authorization) で権限があるかを確認したあと、admission control 段階でそのリクエストを実際に受け入れるかをもう一度検査します。つまり admission controller は、リクエストが etcd に保存される直前に割り込んでリクエストを覗き込む関門です。

Kubernetes には NamespaceLifecycle、LimitRanger のようなビルトイン admission controller がコンパイルされて入っています。しかしユーザーが定義した任意のポリシーを強制するには、外部にリクエストを送って判定を受ける 方式が必要で、これが admission webhook です。

validating vs mutating webhook #

admission webhook は動作によって 2 種類に分かれます。この区分が試験でよく問われる核心です。

種類やること
ValidatingAdmissionWebhookリクエストを検証のみ。通過または拒否latest タグを使った Pod を拒否
MutatingAdmissionWebhookリクエストオブジェクトを変形。フィールドの追加・修正すべての Pod にラベルを自動注入

処理順序も決まっています。API サーバーは mutating webhook を先に呼び出してオブジェクトを変形したあと、validating webhook を呼び出して最終オブジェクトを検証します。変形が先で検証があとなのは、変形で変わった結果を検証がもう一度確認しないとポリシーの一貫性が崩れるからです。

この 2 つの webhook は単に「外部にリクエストを送る」という骨格にすぎず、何を検証して何を変形するかはポリシーエンジンが決定します。そのポリシーエンジンの代表が OPA/Gatekeeper と Kyverno です。

OPA/Gatekeeper #

OPA (Open Policy Agent) は汎用ポリシーエンジンであり、Gatekeeper はその OPA を Kubernetes admission webhook として統合したコンポーネントです。Gatekeeper をインストールすると ValidatingAdmissionWebhook が登録され、入ってくるリクエストを OPA ポリシーで判定します。

Rego と 2 段階のリソース #

Gatekeeper のポリシーは Rego という宣言型クエリ言語で記述します。そしてポリシーを 2 つのカスタムリソースに分けて定義するのが特徴です。

  • ConstraintTemplate: ポリシーのロジックを Rego で定義し、そのポリシーを強制する新しいカスタムリソースの種類 (kind) を作ります。
  • Constraint: ConstraintTemplate が作った kind のインスタンスで、どのリソースにどのパラメータでポリシーを適用するかを指定します。

ロジックと適用を分離するので、一度作った ConstraintTemplate をパラメータだけ変えて複数の Constraint として再利用できます。

例: 信頼レジストリのみ許可 #

まず ConstraintTemplate で「許可されたレジストリで始まるイメージのみを受け入れる」というロジックを定義します。

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sallowedrepos
spec:
  crd:
    spec:
      names:
        kind: K8sAllowedRepos
      validation:
        openAPIV3Schema:
          type: object
          properties:
            repos:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sallowedrepos
        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> uses an untrusted image <%v>", [container.name, container.image])
        }

次に Constraint を作ってこのポリシーを実際に適用します。ここでどのレジストリを許可するかをパラメータで渡します。

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
  name: only-trusted-registry
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
  parameters:
    repos:
      - "registry.example.com/"

これで registry.example.com/ で始まらないイメージを使った Pod を作ろうとすると、admission 段階で拒否されます。kubectl apply の結果に violation メッセージで指定した文言がそのまま出ます。

同じ方式で「特定のラベルがなければ拒否」というポリシーも作れます。Gatekeeper はビルトインの ConstraintTemplate ライブラリを提供するので、K8sRequiredLabels のような一般的なポリシーは自分で Rego を書かずにライブラリのテンプレートを持ってきて、上のような形の Constraint だけ書けば済みます。上の Constraint で kind をライブラリテンプレートが作った kind に、parameters を要求するラベルのリストに変えれば、そのまま動きます。

Kyverno #

Kyverno は OPA/Gatekeeper の代替で、Rego なしで Kubernetes YAML 文法だけでポリシーを記述する ポリシーエンジンです。別のクエリ言語を学ぶ必要がなく、参入障壁が低いです。Kyverno も admission webhook として動作しますが、validate だけでなく mutate と generate まで 1 つのツールで処理します。

機能やること
validateルールに合わないリクエストを拒否または警告
mutateリクエストオブジェクトにフィールドを追加または修正
generateリソース作成時に関連リソースを一緒に作成

例: latest タグ禁止 #

Kyverno のポリシーは ClusterPolicy (またはネームスペース範囲の Policy) で記述します。latest タグを使ったコンテナを拒否するポリシーは次のとおりです。

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
spec:
  validationFailureAction: Enforce
  rules:
    - name: require-image-tag
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "イメージには明示的なタグが必要であり、latest タグは禁止されます"
        pattern:
          spec:
            containers:
              - image: "!*:latest"

validationFailureAction: Enforce が違反リクエストを実際に拒否させるキーです。この値を Audit にすると拒否なしで違反のみを記録します。pattern!*:latest は「latest で終わらないタグ」という意味で、タグなしイメージや latest を使った Pod をすべて防ぎます。

先ほど Gatekeeper で作った信頼レジストリポリシーも Kyverno に移すと、ConstraintTemplate の Rego なしで、同じ ClusterPolicy 構造で patternimage 値だけを "registry.example.com/*" に変えれば済みます。Gatekeeper が 2 つのリソースと Rego を要求したことを、Kyverno はパターン 1 行で終えます。

例: ラベル自動注入 (mutate) #

mutate ポリシーはリクエストを拒否する代わりにオブジェクトを直します。すべての Pod にラベルを自動で付けるポリシーは次のとおりです。

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: add-default-label
spec:
  rules:
    - name: add-team-label
      match:
        any:
          - resources:
              kinds:
                - Pod
      mutate:
        patchStrategicMerge:
          metadata:
            labels:
              team: platform

署名検証の強制とのつながり #

#15 で cosign でイメージに署名を付けました。その署名をクラスター次元で強制する関門がまさに admission control です。Kyverno は verifyImages ルールで cosign 署名を admission 段階で直接検証でき、署名されていないか信頼しないキーで署名されたイメージを拒否します。

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signature
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "registry.example.com/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKo...省略...
                      -----END PUBLIC KEY-----

こうすると #13〜#15 で作った最小イメージ・スキャン・署名の流れが、admission control という最後の関門でポリシーとして強制されます。サプライチェーンセキュリティの結論がポリシー強制で閉じる構造です。

2 つのツールの比較 #

項目OPA/GatekeeperKyverno
ポリシー言語Rego (別途学習が必要)Kubernetes YAML
ポリシーリソースConstraintTemplate + Constraint (2 段階)ClusterPolicy / Policy (1 つ)
validate可能可能
mutate可能 (Assign など)可能 (patchStrategicMerge など)
generate不可可能
イメージ署名検証外部連携が必要verifyImages で内蔵
参入障壁高い (Rego)低い
表現力非常に高い (汎用ポリシー)Kubernetes 限定だが十分

要約すると、Rego の表現力が必要な複雑なポリシーや Kubernetes の外までポリシーを共有しなければならない環境なら Gatekeeper、Kubernetes の中で速く簡単にポリシーを強制したいなら Kyverno です。試験ではどちらが与えられるか分からないので、2 つのツールの基本構造を両方とも知っておくのが安全です。

試験ポイント #

  • validating と mutating の違いと順序 を正確に区別します。mutating が先、validating があとです。変形結果を検証がもう一度確認する順序です。
  • Gatekeeper は ConstraintTemplate でロジックを、Constraint で適用 を定義する 2 段階構造です。2 つのうち 1 つだけ作るとポリシーが動作しません。
  • Kyverno は ClusterPolicy 1 つで終わりvalidationFailureAction: Enforce が違反を実際に拒否させるキーです。Audit は記録のみです。
  • 試験定番は 与えられた違反マニフェストが拒否されるかを確認する 作業です。ポリシーを適用したあと latest タグや信頼しないレジストリを使った Pod を kubectl apply で作ってみて、拒否メッセージが出るかを自分で確認します。
  • ポリシーがすでにデプロイされた状態で 不足しているリソースを見つけて埋める 問題も出ます。ConstraintTemplate だけあって Constraint がなければポリシーが適用されないので、この構造を思い出して足りないほうを作ります。
  • ポリシー作成の文法は公式ドキュメントの閲覧が許可されるので、Gatekeeper と Kyverno のドキュメントの例の場所を事前に身につけておくと作成時間を節約できます。

まとめ #

この記事で押さえたこと:

  • admission controller はリクエストが etcd に保存される前に横取りして検査する関門 です。validating は検証、mutating は変形であり、mutating が先に動作します。
  • OPA/Gatekeeper は Rego ポリシーを ConstraintTemplate と Constraint の 2 段階に分けて定義します。ロジックと適用の分離で再利用が簡単です。
  • Kyverno は Rego なしで Kubernetes YAML だけで validate・mutate・generate をすべて処理します。参入障壁が低く、cosign 署名検証を内蔵します。
  • latest タグ禁止と信頼レジストリ制限を 2 つのツールでそれぞれ実装し、違反マニフェストが拒否されるかを確認する 試験定番のパターンを整理しました。

admission control は #13〜#15 で扱ったサプライチェーンセキュリティをクラスター次元で強制する最後の関門です。ポリシーエンジンのより広い活用は Kubernetes 深掘り #3 で扱ったクラスター運用の文脈ともつながります。

次へ: Falco #

ここまでが予防、つまり危険なワークロードを入れないポリシー強制でした。それでもクラスターの中で起きる異常行動はリアルタイムで検知しなければなりません。

#17 Falco 行動分析、audit logs では、ランタイムでシェル実行・機微ファイルアクセス・予想外のネットワーク接続のような異常行動をルールで検知する Falco の動作原理とルールの書き方、そして Kubernetes audit log を設定して分析する方法まで直接扱いながら整理します。

X