LabHub

ブログ

Kyvernoポリシーエンジン分析: 検証、変形、生成ルールの深層分析

한국어English日本語


1. Validateルール

1.1 パターンマッチング

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-run-as-non-root
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-security-context
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: 'Containers must run as non-root'
        pattern:
          spec:
            containers:
              - securityContext:
                  runAsNonRoot: true

演算子: ?*(空でない値)、*(任意の値、nullを含む)、X|Y(または)、!X(否定)、>X<X>=X<=X(数値比較)

1.2 denyルール

条件ベースでリソースを拒否します:

rules:
  - name: deny-latest-tag
    match:
      any:
        - resources:
            kinds:
              - Pod
    validate:
      message: "Using 'latest' tag is not allowed. Use a specific version tag."
      deny:
        conditions:
          any:
            - key: '{{ request.object.spec.containers[].image }}'
              operator: AnyIn
              value:
                - '*:latest'

1.3 CEL式

Kubernetes 1.25以降のCEL(Common Expression Language)を使います:

rules:
  - name: check-replica-count
    match:
      any:
        - resources:
            kinds:
              - Deployment
    validate:
      cel:
        expressions:
          - expression: 'object.spec.replicas >= 2'
            message: 'Deployment must have at least 2 replicas'
          - expression: 'object.spec.replicas <= 100'
            message: 'Deployment cannot exceed 100 replicas'

1.4 foreach

コレクションの各要素に対して検証します:

rules:
  - name: check-each-container
    match:
      any:
        - resources:
            kinds:
              - Pod
    validate:
      message: 'All containers must have resource limits'
      foreach:
        - list: 'request.object.spec.containers'
          deny:
            conditions:
              any:
                - key: '{{ element.resources.limits.memory }}'
                  operator: Equals
                  value: ''

2. Mutateルール

2.1 patchStrategicMerge

Kubernetes Strategic Merge Patch方式:

rules:
  - name: add-sidecar
    match:
      any:
        - resources:
            kinds:
              - Deployment
            selector:
              matchLabels:
                inject-sidecar: 'true'
    mutate:
      patchStrategicMerge:
        spec:
          template:
            spec:
              containers:
                - name: log-collector
                  image: fluentbit:latest
                  volumeMounts:
                    - name: shared-logs
                      mountPath: /var/log/app
              volumes:
                - name: shared-logs
                  emptyDir: {}

2.2 patchesJson6902

JSON Patch(RFC 6902)方式:

rules:
  - name: add-annotation
    match:
      any:
        - resources:
            kinds:
              - Service
    mutate:
      patchesJson6902: |-
        - op: add
          path: /metadata/annotations/modified-by
          value: kyverno
        - op: replace
          path: /spec/type
          value: ClusterIP

2.3 foreach mutate

rules:
  - name: add-pull-secret-to-all-containers
    match:
      any:
        - resources:
            kinds:
              - Pod
    mutate:
      foreach:
        - list: 'request.object.spec.containers'
          patchStrategicMerge:
            spec:
              imagePullSecrets:
                - name: my-registry-secret

3. Generateルール

3.1 dataベース生成

ポリシーに定義したデータでリソースを生成します:

rules:
  - name: generate-default-limitrange
    match:
      any:
        - resources:
            kinds:
              - Namespace
    generate:
      apiVersion: v1
      kind: LimitRange
      name: default-limits
      namespace: '{{ request.object.metadata.name }}'
      synchronize: true
      data:
        spec:
          limits:
            - default:
                cpu: 500m
                memory: 512Mi
              defaultRequest:
                cpu: 100m
                memory: 128Mi
              type: Container

3.2 cloneベース生成

既存のリソースを複製します:

rules:
  - name: clone-configmap
    match:
      any:
        - resources:
            kinds:
              - Namespace
    generate:
      apiVersion: v1
      kind: ConfigMap
      name: shared-config
      namespace: '{{ request.object.metadata.name }}'
      synchronize: true
      clone:
        namespace: default
        name: template-configmap

3.3 synchronizeオプション

synchronize: true のとき:


4. 変数とコンテキスト

4.1 JMESPath変数

rules:
  - name: add-ns-label
    match:
      any:
        - resources:
            kinds:
              - Deployment
    mutate:
      patchStrategicMerge:
        metadata:
          labels:
            namespace: '{{ request.object.metadata.namespace }}'
            owner: '{{ request.userInfo.username }}'

4.2 API呼び出しコンテキスト

rules:
  - name: check-namespace-labels
    match:
      any:
        - resources:
            kinds:
              - Pod
    context:
      - name: namespaceInfo
        apiCall:
          urlPath: '/api/v1/namespaces/{{ request.namespace }}'
          jmesPath: "metadata.labels.environment || 'unknown'"
    validate:
      message: 'Pods can only run in labeled namespaces'
      deny:
        conditions:
          any:
            - key: '{{ namespaceInfo }}'
              operator: Equals
              value: 'unknown'

4.3 ConfigMapルックアップ

rules:
  - name: check-allowed-registries
    match:
      any:
        - resources:
            kinds:
              - Pod
    context:
      - name: allowedRegistries
        configMap:
          name: allowed-registries
          namespace: kyverno
    validate:
      message: 'Image must be from an allowed registry'
      foreach:
        - list: 'request.object.spec.containers'
          deny:
            conditions:
              all:
                - key: '{{ element.image }}'
                  operator: AnyNotIn
                  value: '{{ allowedRegistries.data.registries }}'

5. 高度なパターン

5.1 条件付きアンカー

# () アンカー: 条件付き — フィールドが存在する場合のみ検証
validate:
  pattern:
    spec:
      template:
        spec:
          containers:
            - (image): "*/nginx:*"  # nginxイメージの場合のみ
              resources:
                limits:
                  memory: ">=256Mi"

# X() 否定アンカー: フィールドが存在してはならない
validate:
  pattern:
    spec:
      template:
        spec:
          containers:
            - name: "*"
              X(securityContext):
                X(privileged): true  # privilegedがtrueであってはならない

5.2 等値アンカー

# =() 等値アンカー
validate:
  pattern:
    spec:
      =(replicas): '>=3' # replicasが設定されていれば3以上

6. validationFailureActionからfailureActionへ

この記事の例はすべて spec.validationFailureAction を使っています。いま動いているクラスタのポリシーはたいていその姿なのでそのまま残しましたが、このフィールドはdeprecatedで、ルール単位のフィールドである spec.rules[*].validate[*].failureAction に移ります。値はEnforceとAuditの2つで、指定しなければAuditです。Enforceは違反したリクエストをブロックし、Auditは通したうえで違反をレポートに残します。

なぜ移したのかがこの変更の核心です。ポリシー単位のフィールドだったころは、1つのポリシーの中のすべてのvalidateルールが同じ強制レベルを共有していました。あるルールは確実だからブロックし、別のルールはまだ観察だけにしたい、という区別をするにはポリシーを2つに割るしかなく、マッチブロックが複製されて管理対象が増えました。ルール単位に降りてくると、1つのポリシーの中でルールAはEnforce、ルールBはAuditにできます。新しいルールを既存のポリシーにAuditで載せて数日レポートを見て、そのルールだけEnforceに上げる流れが、ポリシーを割らずに実現できます。

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-run-as-non-root
spec:
  rules:
    - name: check-security-context
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        # ポリシー単位のvalidationFailureActionではなくルール単位で
        failureAction: Enforce
        message: 'Containers must run as non-root'
        pattern:
          spec:
            containers:
              - securityContext:
                  runAsNonRoot: true

    - name: warn-on-missing-limits
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        # 同じポリシー内でこのルールだけ観察モード
        failureAction: Audit
        message: 'Containers should declare resource limits'
        pattern:
          spec:
            containers:
              - resources:
                  limits:
                    memory: '?*'

一緒に動いたフィールドも知っておくほうがよいです。webhookTimeoutSecondsfailurePolicy も1.13からdeprecatedと表示され、それぞれ webhookConfiguration.timeoutSecondswebhookConfiguration.failurePolicy に移ります。schemaValidation は1.11からdeprecatedで、ドキュメント上いまは何の効果もありません。逆に使われ続けるポリシー単位のフィールドもあります。background は既存リソースを走査して違反を見つけレポートを生成する動作をオンオフし、既定値はtrueです。admission はadmission controlの段階でルールを適用するかを決め、既定値はtrueで、falseにするとbackground専用のポリシーになります。applyRules はマッチしたリソースにルールをいくつ適用するかで、Oneなら最初のマッチで止まり、Allが既定値です。

spec:
  background: true # 既定 true — 既存リソースのスキャンとレポート生成
  admission: true # 既定 true — falseならbackground専用
  applyRules: All # All(既定)または One(最初のマッチで中断)
  webhookConfiguration:
    timeoutSeconds: 20 # 1.13からwebhookTimeoutSecondsの代わりに
    failurePolicy: Fail # 1.13からspec.failurePolicyの代わりに

バージョンによってどちらのフィールドが実際に読まれるかが異なります。正確なフィールドは使用中のバージョンのドキュメントで確認してください。


7. ルールはどこで、どの順序で実行されるのか

Kyvernoは1つのエンジンのように見えますが、ルールの種類ごとに実行される場所が違います。mutateルールはmutating webhookで、validateルールはvalidating webhookで動きます。Kubernetes APIサーバーはmutating admissionを先に呼び、そのあとvalidating admissionを呼ぶので、validateルールが見るオブジェクトはすでにmutateルールが手を入れたあとのオブジェクトです。この順序を知らないと自分のポリシーに自分が引っかかります。サイドカーを注入するmutateポリシーと、すべてのコンテナにリソース制限を要求するvalidateポリシーを一緒にかけておくと、注入されたサイドカーも制限チェックの対象になり、注入スペックに制限を入れていなければデプロイがブロックされ、ログには利用者が書いてもいないコンテナ名が出ます。サイドカー注入ポリシーを作るときにリソース制限も一緒に入れるのは、好みではなく要件です。

generateルールは場所がまったく違います。admissionでリソースを直接作らずUpdateRequestを残し、実際の生成はbackground controllerがあとから行います。そのためネームスペースを作った直後に生成物を確認するとまだ無いことがありますが、これはバグではなく設計です。生成されないときはポリシーを疑う前に、UpdateRequestが残っているかをまず見ます。リクエスト自体が無ければマッチしていないということで、リクエストはあるのにリソースが無ければbackground controller側の問題です。この一度の分岐で調査範囲が半分になります。

kubectl -n kyverno get updaterequests
kubectl auth can-i create helmrepositories --as system:serviceaccount:kyverno:kyverno-background-controller

synchronize: true もタダではありません。ソースが変われば生成物を追従させ、生成物が手で修正されれば元に戻します。つまり対象ネームスペースの数だけ監視と書き込みが増えるということです。ネームスペースが5つのクラスタでは何も感じませんが、数百のクラスタではbackground controllerの常時負荷になります。そしてこのコントローラは最小限の権限だけを持って導入されます。追加の権限は使う側が付ける必要があるので、標準リソースでないものをgenerateし始めるときは、上のauth can-iをそのリソース名に変えて先に確認します。権限が無いとadmissionは成功し、リソースだけが静かに作られません。もっとも気づきにくい種類の失敗です。

最後に、applyRules がOneならマッチしたリソースに最初のルールだけが適用されて止まります。ルールを順に並べたのに後ろのルールがなぜ動かないのかを探しているなら、このフィールドから確認します。


8. ポリシーをローカルで回してからクラスタに載せる

ポリシーはクラスタに載せる前にローカルで回せますし、この習慣ひとつで事故のほとんどが防げます。kyverno apply にポリシーファイルと、--resource で検査対象のマニフェストを渡します。変数を使うポリシーなら --set で1つずつ注入するか、-f で値ファイルを渡します。出力は -t で表、--detailed-results で詳細、-p でレポート形式を選びます。失敗やエラーがあれば終了コードが1になるので、CIのゲートとしてそのまま使えます。ポリシーを直すたびにこのコマンドを回せば、マッチブロックを書き間違えて何にもマッチしないポリシーを作り、通ったと勘違いする事故も防げます。何にもマッチしないポリシーはクラスタで静かに通すだけなので、人の目には正しく動くポリシーと区別がつきません。

kyverno apply policy.yaml --resource pod.yaml
kyverno apply policy.yaml --resource pod.yaml --set namespace=prod,team=payments
kyverno apply policy.yaml --resource pod.yaml -f values.yaml
kyverno apply policy.yaml --resource pod.yaml -t --detailed-results
kyverno apply policy.yaml --resource pod.yaml --policy-report

# 失敗やエラーがあれば 1
echo $?

ローカルの検証を通ったら、同じコマンドをクラスタに向けて回せます。-c は現在のコンテキストのクラスタに接続して検査し、ポリシーをgitソースから直接取得する形もドキュメントが例示しています。既存のリソースが新しいポリシーにどれだけ引っかかるかを先に見たいときにこの組み合わせが役立ちます。ここまで通ったらポリシーを実際に適用し、ポリシー一覧とレポートでクラスタでの姿を確認します。generateルールがあればUpdateRequestまで一緒に見ます。

kyverno apply policy.yaml --cluster
kyverno apply https://github.com/kyverno/policies/openshift/ --git-branch main --cluster

kubectl apply -f policy.yaml
kubectl get cpol,pol -A
kubectl -n kyverno get updaterequests

9. 失敗パターンと診断の順序

ポリシーが期待どおりに動かないとき、いちばん多い間違いはポリシーのYAMLから覗きはじめることです。ポリシーの文法が原因であることは思ったより少なく、多くはポリシーがそもそも評価されていないか、変数が空の値に置換されたケースです。順序を決めておくとずっと早く終わります。

1つ目、ポリシーは準備できているか。ポリシー一覧のready列がすべてtrueかを見ます。ドキュメントが診断の最初の段階として挙げているのがこれです。2つ目、webhookは登録されているか。Kyvernoは2種類のwebhookとして登録され、登録されていなければポリシーは存在してもどのリクエストも通りません。何もブロックされないのにポリシーは問題なさそうに見える状況の大半がここです。

kubectl -n kyverno get po
kubectl get cpol,pol -A
kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations

3つ目、変数が静かに空の値へ置換されるケースです。apiCallのjmesPathが実際のレスポンス構造と合っていないと、エラーではなく空の値が返り、その空の値が条件に入ると条件が常に真か常に偽になります。ポリシーは正常に見えるのに結果だけがおかしい、という典型的な形です。これを目で見るにはログの詳細度を上げる必要があります。ドキュメントは -v=4 を変数置換が見えるレベル、-v=6 を最大の詳細度として案内しています。それでも掴めなければ dumpPayload=true でAdmissionReviewの全文を出力し、実際に届いたオブジェクトが何かを確認します。

kubectl -n kyverno edit deploy kyverno-admission-controller
kubectl -n kyverno logs <pod_name> -f

# クライアント側のスロットリングが見えたらQPSとバーストを上げる
# --clientRateLimitQPS=500 --clientRateLimitBurst=500

4つ目、foreachの list を波括弧で包んだケースです。listはJMESPath式そのものを受け取るので、変数の表記で包みません。この記事のforeachの例が引用符だけを付けているのはそのためです。5つ目、アンカーの誤用です。条件付き ()、等値 =()、存在 ^()、否定 X() はそれぞれ意味が違い、とくに否定アンカーを値の比較と勘違いすることが多いです。否定アンカーはそのキーが存在してはならないという意味で、値が違えばよいという意味ではありません。

6つ目、background controllerの権限です。generateされないときは7節のauth can-iを対象リソースに変えて確認します。7つ目、admission reportが積み上がるケースです。ドキュメントは、reports controllerが正しく動作していないか、admission reportの集計が追いつかないときにレポートが蓄積されると説明しています。まずreports controllerの状態を見て、クライアント側のスロットリングが見えたらQPSとバーストを上げます。

最後は最悪のケースです。ポリシーがAPIサーバーを塞いで何もデプロイできなくなったら、webhook設定を削除するかadmission controllerを0に縮めてクラスタを先に生き返らせます。この2つはポリシー強制を丸ごと切るものなので、原因を直したあと必ず元に戻さなければなりません。


10. 使わないほうがよい場面

検査したいものがフィールド1つの値で、それ以上何も要らないなら、Kubernetesに組み込みのValidatingAdmissionPolicyとCELで十分です。コンポーネントを1つ減らして運用するということは、アップグレード対象が1つ減り、障害時に疑う場所が1つ減り、webhook証明書の更新を気にする対象が1つ減るということです。KyvernoにもCELベースのvalidateはありますが、それはすでにKyvernoを運用しているときに有用な選択肢であって、Kyvernoを導入する理由にはなりません。Kyvernoを正当化するのはgenerateとmutate、そしてイメージ検証のようにin-treeのポリシーではできない仕事です。

mutateで埋めている値が、実はチャートの既定値であるべきケースもよくあります。Helmのvaluesに入れればgitで見えてレビューされタグでロールバックできますが、mutateで入れるとクラスタでしか見えません。半年後になぜこのアノテーションが付いているのか誰も分からない状態になり、マニフェストと実際のオブジェクトが違うという事実がデプロイツールのドリフト検知とずっと戦うことになります。組織全体に強制すべき値だけをmutateに置き、チームが変えられるべき値はチャートに置くほうがよいです。

ワイルドカードで全リソースにマッチするポリシーはとくに注意が必要です。すべてのリクエストがwebhookを通ることになり、クラスタ全体に遅延税をかけるのと同じで、このコストはポリシー1つの性能ではなくAPIサーバーへ来るすべてのリクエストに付きます。タイムアウト内に応答できなければ failurePolicy の既定値Failによってリクエストは拒否されるので、Kyvernoが遅くなった瞬間にクラスタが遅くなるのではなくクラスタが止まります。マッチを必要な種類とネームスペースに絞る作業は、性能チューニングではなく可用性の作業です。

generateとsynchronizeで数百のネームスペースのリソースを管理するのも考え直す価値があります。その仕事はGitOpsのツールのほうが得意で、何よりgitに残ります。Kyvernoのgenerateは、ネームスペース作成のようにadmissionの時点でしか分からないイベントに反応しなければならないときに値打ちがあります。


11. 参考資料


12. まとめ

  1. validate: パターンマッチング、deny条件、CEL式、foreachで多様な検証
  2. mutate: Strategic Merge Patch、JSON Patchでリソースを自動修正
  3. generate: data/cloneベースのリソース自動生成、synchronizeで同期
  4. 変数システム: JMESPath、API呼び出し、ConfigMapルックアップで動的ポリシー
  5. アンカーシステム: 条件付き、否定、等値アンカーで精密なパターンマッチング
  6. 強制レベル: ポリシー単位の validationFailureAction からルール単位の failureAction へ移行
  7. 実行場所: mutateはmutating webhook、validateはvalidating webhook、generateはbackground controller

次の記事ではKyvernoのイメージ検証機能とサプライチェーンセキュリティを扱います。

コメント

まだコメントはありません。

ログインするとコメントできます