LabHub

ブログ

Kyvernoイメージ検証: Sigstoreとサプライチェーンセキュリティ

한국어English日本語


1. verifyImagesルール概要

KyvernoのverifyImagesルールはコンテナイメージの署名とattestation(証明)を検証してサプライチェーンセキュリティを強化します。

1.1 基本構造

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-images
spec:
  validationFailureAction: Enforce
  webhookTimeoutSeconds: 30
  rules:
    - name: verify-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - 'ghcr.io/myorg/*'
            - 'myregistry.io/apps/*'
          attestors:
            - entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
                      -----END PUBLIC KEY-----

2. Cosign署名検証

2.1 静的鍵

verifyImages:
  - imageReferences:
      - 'ghcr.io/myorg/*'
    attestors:
      - entries:
          - keys:
              publicKeys: |-
                -----BEGIN PUBLIC KEY-----
                MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
                -----END PUBLIC KEY-----

2.2 Keyless署名(Fulcio)

OIDCベースのキーレス署名検証:

verifyImages:
  - imageReferences:
      - 'ghcr.io/myorg/*'
    attestors:
      - entries:
          - keyless:
              url: https://fulcio.sigstore.dev
              rekor:
                url: https://rekor.sigstore.dev
              subject: 'https://github.com/myorg/*'
              issuer: 'https://token.actions.githubusercontent.com'

このポリシーは:

2.3 KMS鍵

verifyImages:
  - imageReferences:
      - 'myregistry.io/apps/*'
    attestors:
      - entries:
          - keys:
              kms: awskms:///arn:aws:kms:us-east-1:123456789:key/abc-123
          # または
          - keys:
              kms: gcpkms://projects/my-project/locations/global/keyRings/my-ring/cryptoKeys/my-key
          # または
          - keys:
              kms: azurekms://my-vault.vault.azure.net/keys/my-key

3. Attestation検証

3.1 in-toto Attestation

verifyImages:
  - imageReferences:
      - 'ghcr.io/myorg/*'
    attestations:
      - type: https://slsa.dev/provenance/v1
        attestors:
          - entries:
              - keyless:
                  url: https://fulcio.sigstore.dev
                  rekor:
                    url: https://rekor.sigstore.dev
        conditions:
          - all:
              - key: '{{ builder.id }}'
                operator: Equals
                value: 'https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v1.9.0'

3.2 SLSA Provenance

verifyImages:
  - imageReferences:
      - 'ghcr.io/myorg/*'
    attestations:
      - type: https://slsa.dev/provenance/v1
        attestors:
          - entries:
              - keyless:
                  url: https://fulcio.sigstore.dev
                  subject: 'https://github.com/myorg/*'
                  issuer: 'https://token.actions.githubusercontent.com'
        conditions:
          - all:
              - key: '{{ buildDefinition.buildType }}'
                operator: Equals
                value: 'https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1'
              - key: '{{ runDetails.builder.id }}'
                operator: Equals
                value: 'https://github.com/slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@refs/tags/v1.9.0'

4. イメージレジストリ認証

4.1 プライベートレジストリへのアクセス

# Kyvernoがプライベートレジストリの署名を照会するには
# imagePullSecrets または ServiceAccount にレジストリ認証情報が必要

# values.yaml(Helmインストール時)
# admissionController:
#   container:
#     image:
#       pullSecrets:
#         - name: my-registry-secret

5. SBOM検証

5.1 CycloneDX SBOM attestationの検証

verifyImages:
  - imageReferences:
      - 'ghcr.io/myorg/*'
    attestations:
      - type: https://cyclonedx.org/bom/v1.4
        attestors:
          - entries:
              - keyless:
                  url: https://fulcio.sigstore.dev
        conditions:
          - all:
              - key: "{{ components[?name=='log4j-core'].version | [0] }}"
                operator: NotEquals
                value: '2.14.1'

6. イメージ変形(Mutation)

6.1 タグをダイジェストに変換

verifyImages:
  - imageReferences:
      - 'ghcr.io/myorg/*'
    mutateDigest: true # タグをダイジェストに自動変換
    required: true # 署名が必ず存在すること
    verifyDigest: true # ダイジェスト検証
    attestors:
      - entries:
          - keys:
              publicKeys: |-
                -----BEGIN PUBLIC KEY-----
                ...
                -----END PUBLIC KEY-----

mutateDigest: true はイメージタグをダイジェスト(SHA256)に自動変換し、イメージの不変性を保証します。


7. 実践例: 総合イメージセキュリティポリシー

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: comprehensive-image-security
spec:
  validationFailureAction: Enforce
  webhookTimeoutSeconds: 30
  rules:
    # 1. 許可されたレジストリのみ
    - name: allowed-registries
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: 'Images must be from approved registries'
        foreach:
          - list: 'request.object.spec.[initContainers, containers][]'
            deny:
              conditions:
                all:
                  - key: '{{ element.image }}'
                    operator: AnyNotIn
                    value:
                      - 'ghcr.io/myorg/*'
                      - 'myregistry.io/*'

    # 2. latestタグ禁止
    - name: deny-latest
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Using 'latest' tag is not allowed"
        foreach:
          - list: 'request.object.spec.[initContainers, containers][]'
            deny:
              conditions:
                any:
                  - key: '{{ element.image }}'
                    operator: Equals
                    value: '*:latest'

    # 3. 署名検証
    - name: verify-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - 'ghcr.io/myorg/*'
          mutateDigest: true
          required: true
          attestors:
            - entries:
                - keyless:
                    url: https://fulcio.sigstore.dev
                    subject: 'https://github.com/myorg/*'
                    issuer: 'https://token.actions.githubusercontent.com'

8. admissionで検証が実際に走る順序

上のYAMLは何を検査するかを宣言しているだけで、実際の動作はadmissionリクエストが届いた瞬間に起きます。Pod作成リクエストがAPIサーバーに到達すると、Kyverno admission controllerはPodスペックからコンテナイメージ参照をすべて取り出し、imageReferences のパターンに一致するものだけを選んでレジストリに署名とattestationを要求します。ここで見落としやすいのは、この照会がクラスタの外へ出るネットワーク呼び出しだという点です。ポリシーを有効にした瞬間から、レジストリはイメージをダウンロードする経路であるだけでなく、Podを作れるかどうかを決める経路になります。

署名が常にイメージと同じリポジトリにあるとは限りません。ミラーリングや社内規定で署名だけを別の場所にまとめているなら、repository で照会先を変えます。検証者が複数ある場合は attestors.count が何個通過すべきかを決めます。ドキュメントは、この値を指定しない場合はすべてのattestorを検証すると明記しています。この違いは鍵のローテーション時にそのまま表れます。旧鍵と新鍵の両方をentriesに入れてcountを1にすると、どちらの鍵で署名されたイメージも通るのでローテーション期間を無停止で越えられますが、countを外すと両方の鍵で署名されたイメージだけが通り、正反対の結果になります。

verifyImages:
  - imageReferences:
      - 'ghcr.io/myorg/*'
    skipImageReferences:
      - 'ghcr.io/myorg/legacy-*'
    repository: 'ghcr.io/myorg/signatures' # 署名を別リポジトリから照会
    required: true
    verifyDigest: true
    mutateDigest: true
    attestors:
      - count: 1 # entriesのうち1つ通ればよい(鍵のローテーション期間)
        entries:
          - keys:
              publicKeys: |-
                -----BEGIN PUBLIC KEY-----
                (現在の鍵)
                -----END PUBLIC KEY-----
          - keys:
              publicKeys: |-
                -----BEGIN PUBLIC KEY-----
                (交換予定の鍵)
                -----END PUBLIC KEY-----

同じイメージが繰り返し入ってくるたびにレジストリを往復すると、admissionの遅延がそのままデプロイの遅延になります。Kyvernoは検証結果をTTLキャッシュに保持してこのコストを下げます。ポリシーのフィールドではなくインストールレベルの設定で、既定値はキャッシュ有効がtrue、最大キー数が1000、TTLが60mです。サイズとTTLは0を渡すと既定値に戻ります。このキャッシュのおかげで、レプリカ20個のロールアウトで実際にレジストリ往復のコストを払うのは最初の1回だけです。性能を測ったときに2回目が1回目よりはるかに速く出る理由も、TTLが切れた翌日の最初のデプロイだけが妙に遅い理由もここにあります。キャッシュを切ってadmissionの遅延を測ると、その値はレジストリが落ちたときに見ることになる最悪の姿に近づきます。

# Kyvernoのインストールレベル設定 — ポリシーのフィールドではない
imageVerifyCacheEnabled: true # 既定 true
imageVerifyCacheMaxSize: 1000 # 既定 1000、0を渡すと既定値
imageVerifyCacheTTLDuration: 60m # 既定 60m、0を渡すと既定値

この記事の最初の例が webhookTimeoutSeconds: 30 を指定しているのは偶然ではありません。このフィールドは当該ポリシーの適用に許される最大時間で、ドキュメント上の既定値は10秒、値は1から30秒の間でなければなりません。レジストリ往復が入るポリシーは、既定の10秒では足りなくなる瞬間が来ます。タイムアウトしたときに何が起きるかは failurePolicy が決めます。既定値のFailならリクエストは拒否され、Ignoreなら検証なしで通ります。レジストリが遅くなったときにデプロイ全体が止まるのか、検証が静かに消えるのかが、この1つのフィールドにかかっています。ただし両方とも1.13からdeprecatedと表示され、webhookConfiguration.timeoutSecondswebhookConfiguration.failurePolicy に移ります。使用中のバージョンのドキュメントで、どちらが実際に読まれるかを先に確認してください。

mutateDigest: true がロールアウトに与える影響は、有効にした日にすぐ目につきます。ポリシーがタグをダイジェストに書き換えるため、Deploymentを適用したあとPodスペックに残るのはタグではなく @sha256: で始まる固定された参照です。レジストリで同じタグを上書きしても、すでに動いているPodはもちろん、スケールアウトで新しく起動するPodまで最初に固定されたイメージを使います。それが本来意図した不変性ですが、タグを上書きしてPodを削除し新しいイメージを取得させていたパイプラインは、この時点から静かに何もしなくなります。mutateDigestを有効にした翌日に最初に届く問い合わせは、たいていこれです。

残りの3つのフィールドは役割が明確です。required は一致したイメージがすべて検証を通ったことを強制し、verifyDigest はダイジェストの使用そのものを強制し、skipImageReferences は一致から外すパターンのリストです。例外を作るためにポリシーを丸ごと複製せず、ここに書くほうが管理しやすくなります。そして imageReferences に変数の展開は入りません。ドキュメントが明示的に定めている制約なので、ネームスペースのラベルによって許可レジストリを変えるようなポリシーはこのフィールドだけでは作れず、ポリシーを分けるか別のvalidateルールで処理する必要があります。


9. 最初から最後まで: 署名し、ポリシーをかけ、ブロックされるのを見る

出発点はクラスタではなくローカルです。cosignで鍵ペアを作ってイメージに署名し、同じ鍵の公開部分で検証まで手元で通しておきます。ここで失敗するなら、ポリシーをいくら直しても通りません。attestationも使う計画なら、predicateファイルを添えてattestし、verify-attestation で確認するところまでローカルで終わらせておきます。

cosign generate-key-pair
cosign sign --key cosign.key ${IMAGE}
cosign verify --key cosign.pub ${IMAGE}

# attestationまで検証する計画なら
cosign attest --key cosign.key --predicate <file> --type <predicate type> ${IMAGE}
cosign verify-attestation --key cosign.pub --type <type> ${IMAGE}

次はポリシーをクラスタに入れる前に確認する段階です。kyverno apply はポリシーとリソースをローカルで噛み合わせて実行します。イメージ検証ポリシーはレジストリに届いてこそ意味があるので --registry を付けますが、ドキュメントはこのフラグをローカルのdocker認証情報でイメージレジストリにアクセスするオプションだと説明しています。結果を整理して見たいなら -t、ルール単位まで展開したいなら --detailed-results、レポート形式で受け取りたいなら -p を使います。失敗やエラーがあればコマンドは1で終了するので、CIジョブにそのまま組み込んでおけば、署名のないイメージがマニフェストに入った瞬間にパイプラインが止まります。ポリシーがクラスタにデプロイされる前に捕まるのと、デプロイ後に捕まるのとでは差が大きいです。

kyverno apply policy.yaml --resource pod.yaml --registry
kyverno apply policy.yaml --resource pod.yaml --registry -t
kyverno apply policy.yaml --resource pod.yaml --registry --detailed-results
kyverno apply policy.yaml --resource pod.yaml --registry --policy-report

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

ローカルで期待どおりの結果になったら、ポリシーを適用し、署名していないイメージをわざと投入して実際にブロックされるか見ます。拒否はAPIサーバーがwebhookの応答をそのまま返す形で届き、メッセージにはどのポリシーのどのルールがブロックしたかと失敗理由が入ります。署名がまったくなければ signature not found 系、署名はあるが鍵が合わなければ invalid signature 系の文が付きます。この2つを見分けるところが次節の診断の出発点です。

kubectl apply -f policy.yaml
kubectl get cpol,pol -A
kubectl apply -f unsigned-pod.yaml

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

症状はたいてい1つに収束します。Podが起動しない、です。ところが原因は5つの枝に分かれ、順序を守らないと見当違いの場所を長く掘ることになります。診断はつねに外から内へ、つまりポリシーがそもそも動いているかを確認してから署名そのものへ降りていきます。

1つ目はポリシーが実際に評価されているかです。Podが通ったからポリシーが正しく動いているのではなく、ポリシーが動いていないだけかもしれません。Podの状態を見て、ポリシー一覧のready列がすべてtrueかを見て、webhookが登録されているかを見ます。Kyvernoは2種類のwebhookとして登録され、mutateDigest を使うイメージ検証ポリシーはmutating側にもかかります。

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

2つ目は、署名がないのか合わないのかの区別です。前節の cosign verify をそのまま手で回してみます。手でも失敗するならクラスタの問題ではなくビルドパイプラインの問題で、手では通るのにクラスタでだけ失敗するなら、そこからKyverno側を見ます。この一度の確認で半分の事例がふるい落とされます。

3つ目はレジストリ認証です。Kyverno自身がプライベートレジストリを読めないと、署名が問題なく存在していても存在しないように見えます。Podがイメージを問題なく取得できることと、Kyvernoが署名を照会することは、まったく別の認証経路だというのが落とし穴です。ドキュメントはkyvernoネームスペースにdocker-registryシークレットを作り、Kyverno Deploymentに --imagePullSecrets で渡す方法を案内しています。ポリシー単位で別の認証情報を使うなら imageRegistryCredentials があります。社内CAで署名されたレジストリなら証明書の信頼という別の問題があり、ドキュメントは global.caCertificates.data で証明書ストアを置き換えるか、global.caCertificates.volume でホストの証明書をマウントする2つの方法を示しています。

4つ目はattestorの不一致です。署名はあり照会もできるのに検証だけ失敗するケースで、keylessで特に多く起きます。subjectとissuerはワークフローのパスとタグまで正確に一致しなければならず、リリースワークフローのファイル名を変えたりタグの規則を変えたりすると、その瞬間からすべてブロックされます。そして attestors.count を指定していないかどうかをもう一度確認します。指定がなければentriesのすべてが通る必要があります。鍵を1つ追加した途端に全部失敗するようになったなら、まず間違いなくこれです。

5つ目はRekorに到達できないことです。閉域網やアウトバウンドプロキシのある環境で透明性ログの照会が塞がれると、署名と鍵が完璧でも検証が終わりません。rekorignoreTlog をtrueにすると透明性ログの検証をスキップし、ctlogignoreSCT をtrueにするとSCTの検証をスキップします。ただしこれは回避策ではなく検証範囲を縮める選択なので、何を諦めたのかをポリシーのコメントに残しておくほうがよいです。

ここまで来ても掴めないならログを増やします。-v=4 は変数置換の過程を見せ、-v=6 が最大の詳細度で、dumpPayload=true はAdmissionReviewの全文を出力します。最後のものはログ量が相当なので、再現の直前に有効化してすぐ切るほうがよいです。そしてポリシーがAPIサーバー自体を塞いで何もデプロイできない最悪の状況になったら、コントローラを0に縮めるかwebhook設定を削除してクラスタを先に生き返らせ、それから原因を見ます。この2つのコマンドはクラスタのポリシー強制をしばらく丸ごと切るものなので、復旧後は必ず元に戻さなければなりません。

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

# 最後の手段 — ポリシー強制がしばらく丸ごと切れる
kubectl scale deploy kyverno-admission-controller -n kyverno --replicas 0
kubectl delete validatingwebhookconfiguration kyverno-resource-validating-webhook-cfg
kubectl delete mutatingwebhookconfiguration kyverno-resource-mutating-webhook-cfg

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

ミラーレジストリにイメージだけをコピーして署名アーティファクトを一緒に移していないなら、このポリシーは例外なくすべて失敗します。閉域網へイメージを持ち込むときに最もよく起きることです。イメージは社内レジストリにあるのに署名は元のレジストリにしか残っていないので、repository で署名の場所を別途指定するか、ミラーリングのパイプラインが署名まで運ぶよう直すまではEnforceを有効にしてはいけません。ここにRekor照会まで必要なkeyless構成なら、外部アクセスが遮断された環境ではそもそも成立しません。

自分が作っていないイメージにも適用しづらいです。サードパーティのイメージは署名しないか、していても自分が信頼関係を結んでいない鍵で署名します。この場合は skipImageReferences で外すかポリシーを分けることになりますが、例外リストが実際に動いているイメージの半分を超えると、そのポリシーはセキュリティを与えるのではなくセキュリティがあるという錯覚を与えます。その状態なら、いっそ許可レジストリの検査だけを残し、署名検証は社内ビルドの成果物にだけかけるほうが正直です。

開発クラスタに最初からEnforceでかけるのも避けるべきです。署名パイプラインがまだすべてのチームに行き渡っていない状態で強制モードを有効にすると、デプロイが丸ごと止まり、結局ポリシーではなくポリシーを作った人がボトルネックになります。Auditで数日回してレポートを見て、引っかかるイメージの一覧が想定と一致したときにEnforceへ上げます。

最後に、レジストリをadmission経路の依存にするという事実そのものがコストです。failurePolicy が既定値のFailならレジストリが遅くなっている間はPod作成が止まり、Ignoreにしておけばその間は検証がないのと同じです。ドキュメントはIgnoreならレジストリ障害が作業を塞がず、イメージがすでにノードにある状況で有用だと説明していますが、それは同時に障害区間で署名のないイメージが入りうるという意味でもあります。どちらもタダではなく、この選択はセキュリティチームだけでなくサービス可用性を持つ側と一緒に決めるべきものです。


12. 参考資料


13. まとめ

  1. cosign検証: 静的鍵、Keyless(Fulcio)、KMSサポート
  2. Attestation検証: in-toto、SLSA provenance条件ベースの検証
  3. SBOM検証: CycloneDX/SPDX attestation内の脆弱コンポーネント確認
  4. ダイジェスト変換: mutateDigestでイメージの不変性を保証
  5. 総合ポリシー: レジストリ制限 + タグポリシー + 署名検証の組み合わせ
  6. 運用パラメータ: 検証キャッシュ、webhookTimeoutSecondsfailurePolicy がadmissionの遅延と障害時の動作を決める
  7. 診断の順序: ポリシーの動作確認、署名の有無、レジストリ認証、attestorの一致、Rekorへの到達性

次の記事ではKyvernoとOPA/Gatekeeperの比較分析を扱います。

コメント

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

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