TT Lab
はじめる
学ぶ 学習パス コース

ポリシーをコードで

タグは同じなのに、立ち上がったのは昨日のものではなかった

TT Labで続きを見る

目標

イメージ参照を形ごとに見分ける目を作り、ダイジェストの固定を要求するルールを、アドミッションに警告として付けた後、狭い場所にだけブロックとして上げ、タグをダイジェストに書き換えるロックファイルとリゾルバーを書き、同じルールをクラスター外のビルドのアーティファクトにもかけてみます。

なぜ重要なのか

タグは付け替えられるラベルで、ダイジェストは内容のハッシュなので付け替えられません。イメージポリシーのほとんどすべてが、この一文の系です。同じタグに新しいイメージをプッシュすることをレジストリが防がないため、タグだけでデプロイしたチームは、「昨日と今日が違う」事故を再現できず、「戻す対象が消えた」ロールバックに出会います。そのため、ルールの最初の1行は、latestの禁止ではなくダイジェストの固定です。ただし、固定が保証するのは、内容の同一性1つだけです。その内容が安全かどうか、誰が作ったか、どう作ったかは、ハッシュの中になく、脆弱性検査と署名と来歴証明が、それぞれ別の軸で、その場所を満たします。ルールを書く側にも、落とし穴が2つあります。コンテナのリストは、spec.containers1つではなく、initContainersとephemeralContainersまで3つあり、イメージ参照は、Podにだけあるのではなく、ワークロードのPodテンプレートにも、1段深いパスで入っています。この2つを抜かしたポリシーは、オンにしてあってもすり抜けられます。そして、固定は更新の手順とセットです。ロックファイルが、その手順の住む場所です。

ステップ

  1. /root/imgpolicyで作業します。/root/imgpolicy/manifests/に、マニフェストを4つ作成してください。web.yamlはPod probe-web(initContainers migrateはregistry.internal/migrate:2.1、containers webはregistry.internal/api:1.4.0)、cache.yamlはPod probe-cache(containers cacheはregistry.internal/sidecar@sha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266)、batch.yamlはPod probe-batch(containers batchはタグのないregistry.internal/runtime、tailはbusybox:latest)、api.yamlはDeployment probe-api(テンプレートのコンテナapiはregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc)です。そのあと、/root/imgpolicy/classify.sh <디렉터리>(プレースホルダーはディレクトリです)を作成してください。そのディレクトリの.yaml・.ymlからイメージ参照をすべて取り出し、重複なしでソートして、1行に<참조> <모양>(プレースホルダーは参照と形です)として出力します。形は、正確に4つの語digest・latest・tag・notagのうちの1つです。最後に、./classify.sh manifests > /root/imgpolicy/01-shapes.txtで、結果を保存してください。

  2. export KUBECONFIG=/root/.kube/configとkubectl config use-context kwok-labで始めます。/root/imgpolicy/policy.yamlに、admissionregistration.k8s.io/v1のValidatingAdmissionPolicy require-image-digestを書いてください。matchConstraints.resourceRulesは、コアグループ("")のv1のpodsに対するCREATE・UPDATEを捉え、変数floatingは、object.spec.containersのイメージのうち、@sha256:がないものの一覧で、validationはsize(variables.floating) == 0で、messageExpressionがその一覧を含みます。ネームスペースimg-warnを作成して、ラベルimage-policy=warnを付けてください。/root/imgpolicy/binding-warn.yamlに、ValidatingAdmissionPolicyBinding image-digest-warnを書いてください。policyNameはrequire-image-digest、validationActionsは["Warn", "Audit"]、matchResources.namespaceSelector.matchLabelsはimage-policy: warnです。テスト用のPod /root/imgpolicy/pod-tag.yamlを作成してください。Pod probe-tag、initContainers migrateはregistry.internal/migrate:2.1、containers webはregistry.internal/api:1.4.0です。ポリシーとバインディングを適用した後、img-warnにpod-tag.yamlを--dry-run=serverで送って、出力を/root/imgpolicy/02-warn.txtに、標準エラー出力まで一緒に保存してください。警告が出るのに、Podは作られる必要があります。

  3. まず、/root/imgpolicy/pod-init.yamlを作成してください。Pod probe-init、initContainers migrateはregistry.internal/migrate:2.1(固定されていない)、containers appはregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffcです。これをimg-warnに--dry-run=serverで送ってみると、警告が出ません。そのあと、/root/imgpolicy/policy.yamlを修正してください。変数allImagesが、spec.containersとspec.initContainersとspec.ephemeralContainersをすべてつなげたイメージの一覧になるようにし(ないかもしれないフィールドは、has(...)で先に確認)、floatingは、そのうち@sha256:がないものだけを残します。matchConstraintsのresourcesに、pods/ephemeralcontainersも追加してください。そして、/root/imgpolicy/pod-pinned.yamlにPod probe-pinned(containers appはregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc)を作成して、img-warnに実際に作成してください(kubectl apply -n img-warn -f pod-pinned.yaml)。エフェメラルコンテナは、Podを作成するときには付けられないため、サブリソースとして送ります。

    kubectl get -n img-warn pod probe-pinned -o json | jq '.spec.ephemeralContainers = [{"name":"dbg","image":"busybox:latest"}]' | kubectl replace --raw "/api/v1/namespaces/img-warn/pods/probe-pinned/ephemeralcontainers?dryRun=All" -f -

修正したポリシーを適用した後、(1)pod-init.yamlをimg-warnにもう一度送り、(2)上のエフェメラルコンテナのリクエストを送って、2つの出力を/root/imgpolicy/03-lists.txtに、標準エラー出力まで一緒に保存してください。今度は、どちらも警告が出る必要があります。 4. ネームスペースをさらに2つ作成してください。img-prodにはラベルimage-policy=enforceを付け、img-legacyには、ラベルを何も付けません(範囲外)。警告用のバインディングは、削除せずにそのままにしておきます。/root/imgpolicy/binding-deny.yamlに、2つ目のバインディングimage-digest-denyを書いてください。同じpolicyNameで、validationActionsは["Deny"]、セレクターはimage-policy: enforceです。続けて、/root/imgpolicy/policy.yamlのmatchConstraintsに、appsグループv1のdeploymentsに対するCREATE・UPDATEのルールを追加し、変数podSpecを置いて、has(object.spec.template)ならobject.spec.template.specを、そうでなければobject.specを選ぶようにしてください。allImagesは、今度はvariables.podSpecの3つのリストから取り出します。/root/imgpolicy/dep-tag.yamlに、Deployment probe-dep(テンプレートのコンテナapiはregistry.internal/api:1.4.0)を作成し、img-prodとimg-legacyに順に--dry-run=serverで送って、2つの出力を/root/imgpolicy/04-deny.txtに保存してください。img-prodでは拒否され、img-legacyではそのまま作られる必要があります。 5. /root/imgpolicy/images.lockに、ロックファイルをJSONオブジェクト1つとして書いてください。キーはタグまで付いた参照で、値はダイジェストです。registry.internal/api:1.4.0はsha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc、registry.internal/migrate:2.1はsha256:3e7c7af932c7b5e39c7812b321bbbd291da943717e96860e5976ac609ef84030、registry.internal/runtime:3.20はsha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2c、registry.internal/sidecar:1.0はsha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266です。そのあと、リゾルバー/root/imgpolicy/resolve.sh <매니페스트> [잠금파일](プレースホルダーはマニフェストとロックファイルです)を作成してください。ロックファイルの引数を省略すると、/root/imgpolicy/images.lockを使います。マニフェストを読んで、タグの参照を<이름>@<다이제스트>(プレースホルダーは名前とダイジェストです)に置き換えたマニフェスト全体を標準出力に出力し、すでにダイジェストで固定された行は、そのまま流します。ロックファイルにない参照は、ダイジェストを作り出さずに、標準エラー出力にLOCK MISS <참조>(プレースホルダーは参照です)を1行ずつ出力して、終了コード3で終了します(そのとき、標準出力には何も出力しません)。作成した後、./resolve.sh manifests/web.yaml > /root/imgpolicy/resolved/web.yamlで、解決結果を保存し、./resolve.sh manifests/batch.yamlの標準エラー出力を/root/imgpolicy/05-miss.txtに保存した後、そのファイルの末尾にEXIT <종료코드>(プレースホルダーは終了コードです)を1行追記してください。 6. /root/imgpolicy/Dockerfileを書いてください。最初のステージはFROM registry.internal/builder:3.2 AS build、2つ目のステージはFROM registry.internal/runtime@sha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2cです。/root/imgpolicy/build.jsonに、ビルドのアーティファクトの一覧を書いてください。キーは3つです。build(文字列api-2026-09-17)、base_images(DockerfileのFROM参照を、書かれた順序のまま入れた文字列の配列)、artifacts(nameとimageを持つオブジェクトの配列。apiはregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc、workerはregistry.internal/worker:2.0)です。/root/imgpolicy/policies/image-refs.yamlに、apiVersion: json.kyverno.io/v1alpha1・kind: ValidatingPolicyのポリシーを書いてください。ルールは2つです。base-pinnedは、base_imagesのうち@sha256:がないものの数が0である必要があり、artifact-pinnedは、artifacts[].imageのうち@sha256:がないものの数が0である必要があります。そのあと、ゲート/root/imgpolicy/jsongate.sh <페이로드> [정책](プレースホルダーはペイロードとポリシーです)を作成してください(ポリシーの引数を省略すると/root/imgpolicy/policies/image-refs.yaml)。kyverno json scanの人が読む出力をパースして、違反1行ごとにFAIL <그 줄>(プレースホルダーはその行です)を出力し、最後にRESULT failed=<개수>(プレースホルダーは個数です)を出力した後、違反があれば終了コード1、なければ0で終了します。判定の行(PASSED・FAILED・ERROR:)が1つもなければ、通過とみなさずに、終了コード2で終了します。最後に、/root/imgpolicy/06-gate.txtを作成してください。1行目は、ポリシーをそのまま実行したときの終了コードを書いたSCAN EXIT <코드>(プレースホルダーはコードです)、その下に./jsongate.sh build.jsonの出力、最後の行はGATE EXIT <코드>(プレースホルダーはコードです)です。 7. ダイジェストが保証するのは、内容の同一性1つです。保証しない3つのことを、別の軸の仕組みが満たします。/root/imgpolicy/limits.tsvに、ちょうど3行を書いてください。各行は、タブ1つで区切られた2つのフィールドで、1つ目のフィールドはcontent-safety・author・build-pathの3つが1回ずつ、2つ目のフィールドは、その場所を満たす仕組みとして、scan・signature・provenanceのうち適切なもの1つです(1つ目のフィールドの順序は、この順序で書きます)。そして、/root/imgpolicy/revoked.txtに、取り消されたダイジェストを1行に1つずつ書いてください。今は1行だけです。registry.internal/sidecar:1.0が指すダイジェストが脆弱だと判明したので、その値を/root/imgpolicy/images.lockから探して、そのまま書きます(@の前の名前は書かず、sha256:で始まる値だけを書きます)。 8. /root/imgpolicy/audit.sh <매니페스트디렉터리> [폐기목록](プレースホルダーはマニフェストのディレクトリと取り消しリストです)を作成してください。取り消しリストの引数を省略すると、/root/imgpolicy/revoked.txtを使います。そのディレクトリの.yaml・.ymlからイメージ参照をすべて取り出し、ダイジェストで固定されていない参照ごとにFLOATING <파일이름> <참조>(プレースホルダーはファイル名と参照です)を、固定はされているがそのダイジェストが取り消しリストにあればREVOKED <파일이름> <참조>(プレースホルダーはファイル名と参照です)を、1行ずつ出力します(それらの行はソートして出力します)。最後の行は、正確にRESULT floating=<수> revoked=<수> ready=<yes|no>(プレースホルダーは数と、yesまたはnoです)で、両方とも0のときだけready=yesです。終了コードは、ready=yesなら0、そうでなければ1です。作成した後、./audit.sh manifestsの出力を/root/imgpolicy/audit.txtに保存し、そのファイルの末尾にEXIT <종료코드>(プレースホルダーは終了コードです)を1行追記してください。ステップ1で作ったマニフェストのまとまりが、そのまま対象です。

参考

同じまとまりの中に、4つの形のイメージ参照がある

/root/imgpolicyで作業します。/root/imgpolicy/manifests/に、マニフェストを4つ作成してください。web.yamlはPod probe-web(initContainers migrateはregistry.internal/migrate:2.1、containers webはregistry.internal/api:1.4.0)、cache.yamlはPod probe-cache(containers cacheはregistry.internal/sidecar@sha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266)、batch.yamlはPod probe-batch(containers batchはタグのないregistry.internal/runtime、tailはbusybox:latest)、api.yamlはDeployment probe-api(テンプレートのコンテナapiはregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc)です。そのあと、/root/imgpolicy/classify.sh <디렉터리>(プレースホルダーはディレクトリです)を作成してください。そのディレクトリの.yaml・.ymlからイメージ参照をすべて取り出し、重複なしでソートして、1行に<참조> <모양>(プレースホルダーは参照と形です)として出力します。形は、正確に4つの語digest・latest・tag・notagのうちの1つです。最後に、./classify.sh manifests > /root/imgpolicy/01-shapes.txtで、結果を保存してください。

イメージ参照は、<레지스트리>/<이름>[:태그][@sha256:...](プレースホルダーはレジストリ、名前、タグです)です。ダイジェストが付いていれば、タグが一緒にあっても、取得するときに使われるのはダイジェストだけなので、digestとみなします。タグがあるかないかを、参照全体で:を探して判定すると、registry.internal:5000/team/appのように、ホストにポートが付いた参照を、タグのあるものと誤って見てしまいます。最後の/の後ろだけを見て判定してください。シェルで最後の部分は、${ref##*/}で得られます。

ダイジェストを要求するルールを、警告として付ける

export KUBECONFIG=/root/.kube/configとkubectl config use-context kwok-labで始めます。/root/imgpolicy/policy.yamlに、admissionregistration.k8s.io/v1のValidatingAdmissionPolicy require-image-digestを書いてください。matchConstraints.resourceRulesは、コアグループ("")のv1のpodsに対するCREATE・UPDATEを捉え、変数floatingは、object.spec.containersのイメージのうち、@sha256:がないものの一覧で、validationはsize(variables.floating) == 0で、messageExpressionがその一覧を含みます。ネームスペースimg-warnを作成して、ラベルimage-policy=warnを付けてください。/root/imgpolicy/binding-warn.yamlに、ValidatingAdmissionPolicyBinding image-digest-warnを書いてください。policyNameはrequire-image-digest、validationActionsは["Warn", "Audit"]、matchResources.namespaceSelector.matchLabelsはimage-policy: warnです。テスト用のPod /root/imgpolicy/pod-tag.yamlを作成してください。Pod probe-tag、initContainers migrateはregistry.internal/migrate:2.1、containers webはregistry.internal/api:1.4.0です。ポリシーとバインディングを適用した後、img-warnにpod-tag.yamlを--dry-run=serverで送って、出力を/root/imgpolicy/02-warn.txtに、標準エラー出力まで一緒に保存してください。警告が出るのに、Podは作られる必要があります。

ポリシーは「何を判定するか」だけを決め、「どこにどの強度で」はバインディングが決めます。Warnは、レスポンスのヘッダーで警告だけを返して、リクエストは通過させます。ロールアウトの最初の段階をWarnにする理由は、何が引っかかるかを先に数えるためです。CELの文字列にはcontains・startsWith・endsWithがあり、リストにはmap・filter・joinがあります。警告は標準エラー出力に出るので、2>&1を付けないと、ファイルに一緒に入りません。kubectl create -f <파일> --dry-run=server(プレースホルダーはファイルです)は、アドミッションをそのまま通しつつ、オブジェクトは残しません。拒否メッセージも警告も、本物のリクエストとまったく同じように出ます。

コンテナのリストを1つだけ見ると、そのまますり抜けられる

まず、/root/imgpolicy/pod-init.yamlを作成してください。Pod probe-init、initContainers migrateはregistry.internal/migrate:2.1(固定されていない)、containers appはregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffcです。これをimg-warnに--dry-run=serverで送ってみると、警告が出ません。そのあと、/root/imgpolicy/policy.yamlを修正してください。変数allImagesが、spec.containersとspec.initContainersとspec.ephemeralContainersをすべてつなげたイメージの一覧になるようにし(ないかもしれないフィールドは、has(...)で先に確認)、floatingは、そのうち@sha256:がないものだけを残します。matchConstraintsのresourcesに、pods/ephemeralcontainersも追加してください。そして、/root/imgpolicy/pod-pinned.yamlにPod probe-pinned(containers appはregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc)を作成して、img-warnに実際に作成してください(kubectl apply -n img-warn -f pod-pinned.yaml)。エフェメラルコンテナは、Podを作成するときには付けられないため、サブリソースとして送ります。

kubectl get -n img-warn pod probe-pinned -o json | jq '.spec.ephemeralContainers = [{"name":"dbg","image":"busybox:latest"}]' | kubectl replace --raw "/api/v1/namespaces/img-warn/pods/probe-pinned/ephemeralcontainers?dryRun=All" -f -

修正したポリシーを適用した後、(1)pod-init.yamlをimg-warnにもう一度送り、(2)上のエフェメラルコンテナのリクエストを送って、2つの出力を/root/imgpolicy/03-lists.txtに、標準エラー出力まで一緒に保存してください。今度は、どちらも警告が出る必要があります。

Podには、コンテナのリストが3つあります。containers・initContainers・ephemeralContainersです。実務で最もよくあるポリシーの穴が、最初のリストだけを見てしまうことです。CELでは、リストは+でつなげられ、ないかもしれないフィールドは、has(x) ? x : []の形の三項式で包みます。ephemeralContainersは、PodのCREATEでは設定できず(Forbidden: cannot be set on create)、pods/ephemeralcontainersサブリソースからしか入ってきません。そのため、ポリシーのresourcesにそのサブリソースを書いてはじめて、kubectl debugのような経路まで、同じルールが見ます。?dryRun=Allを付けたので、このリクエストは判定だけを受けて、Podには何も残しません。

ブロックは広くオンにせず、狭い場所にだけオンにする

ネームスペースをさらに2つ作成してください。img-prodにはラベルimage-policy=enforceを付け、img-legacyには、ラベルを何も付けません(範囲外)。警告用のバインディングは、削除せずにそのままにしておきます。/root/imgpolicy/binding-deny.yamlに、2つ目のバインディングimage-digest-denyを書いてください。同じpolicyNameで、validationActionsは["Deny"]、セレクターはimage-policy: enforceです。続けて、/root/imgpolicy/policy.yamlのmatchConstraintsに、appsグループv1のdeploymentsに対するCREATE・UPDATEのルールを追加し、変数podSpecを置いて、has(object.spec.template)ならobject.spec.template.specを、そうでなければobject.specを選ぶようにしてください。allImagesは、今度はvariables.podSpecの3つのリストから取り出します。/root/imgpolicy/dep-tag.yamlに、Deployment probe-dep(テンプレートのコンテナapiはregistry.internal/api:1.4.0)を作成し、img-prodとimg-legacyに順に--dry-run=serverで送って、2つの出力を/root/imgpolicy/04-deny.txtに保存してください。img-prodでは拒否され、img-legacyではそのまま作られる必要があります。

ポリシー1つに、バインディングを複数付けられ、各バインディングが、自分の範囲と自分の強度を持ちます。そのため、「広くかけて例外を掘る」のではなく、「狭くかけて広げていく」ロールアウトになります。例外の一覧は、時間が経っても誰も消しませんが、狭い範囲は、広げるたびに決定が必要です。イメージ参照は、Podにだけあるのではありません。Deployment・StatefulSet・Jobは、Podテンプレートの中に同じ文字列を持っていて、パスが1段深いです。Podだけを捉えるポリシーも、結局はコントローラーが作ったPodをブロックしますが、そのときはすでにデプロイが始まった後なので、人が原因を探しにくくなります。CELの三項式で、2つの形を1つの変数に入れれば、validationは1つで済みます。

タグをダイジェストに書き換えておき、それだけを見て直す

/root/imgpolicy/images.lockに、ロックファイルをJSONオブジェクト1つとして書いてください。キーはタグまで付いた参照で、値はダイジェストです。registry.internal/api:1.4.0はsha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc、registry.internal/migrate:2.1はsha256:3e7c7af932c7b5e39c7812b321bbbd291da943717e96860e5976ac609ef84030、registry.internal/runtime:3.20はsha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2c、registry.internal/sidecar:1.0はsha256:0e53d844ccfccd2bbb572085b68f6b170cdcfa4bc86cb7cf407c52fc64f11266です。そのあと、リゾルバー/root/imgpolicy/resolve.sh <매니페스트> [잠금파일](プレースホルダーはマニフェストとロックファイルです)を作成してください。ロックファイルの引数を省略すると、/root/imgpolicy/images.lockを使います。マニフェストを読んで、タグの参照を<이름>@<다이제스트>(プレースホルダーは名前とダイジェストです)に置き換えたマニフェスト全体を標準出力に出力し、すでにダイジェストで固定された行は、そのまま流します。ロックファイルにない参照は、ダイジェストを作り出さずに、標準エラー出力にLOCK MISS <참조>(プレースホルダーは参照です)を1行ずつ出力して、終了コード3で終了します(そのとき、標準出力には何も出力しません)。作成した後、./resolve.sh manifests/web.yaml > /root/imgpolicy/resolved/web.yamlで、解決結果を保存し、./resolve.sh manifests/batch.yamlの標準エラー出力を/root/imgpolicy/05-miss.txtに保存した後、そのファイルの末尾にEXIT <종료코드>(プレースホルダーは終了コードです)を1行追記してください。

ロックファイルは、「今このタグが何を指しているか」を、人が読めるように書いておいた表です。デプロイはダイジェストで行い、更新はこの表を直すことで行います。固定と更新は、セットです。jq -r --arg k "$ref" '.[$k] // empty' <파일>(プレースホルダーはファイルです)で、キー1つを安全に探せます。参照からタグを取り除くには、最後の/の後ろの:から先を削除する必要があります。sed 's/:[^:/]*$//'が、ホストのポートを触りません。リゾルバーがロックファイルを本当に読んでいるかどうかは、別のロックファイルを与えればすぐにわかります。このラボの採点ツールが、そうします。

同じルールを、クラスター外のビルドのアーティファクトにもかける

/root/imgpolicy/Dockerfileを書いてください。最初のステージはFROM registry.internal/builder:3.2 AS build、2つ目のステージはFROM registry.internal/runtime@sha256:5d964cb8b51a568d1048bc41dc78327a4bd4548cbbb8bee3c1072e9318c5ca2cです。/root/imgpolicy/build.jsonに、ビルドのアーティファクトの一覧を書いてください。キーは3つです。build(文字列api-2026-09-17)、base_images(DockerfileのFROM参照を、書かれた順序のまま入れた文字列の配列)、artifacts(nameとimageを持つオブジェクトの配列。apiはregistry.internal/api@sha256:34b9b8b35a4fa7fd1d0e7a8b5736738c7b9d3ae21dfc1eec733bc2122cabeffc、workerはregistry.internal/worker:2.0)です。/root/imgpolicy/policies/image-refs.yamlに、apiVersion: json.kyverno.io/v1alpha1・kind: ValidatingPolicyのポリシーを書いてください。ルールは2つです。base-pinnedは、base_imagesのうち@sha256:がないものの数が0である必要があり、artifact-pinnedは、artifacts[].imageのうち@sha256:がないものの数が0である必要があります。そのあと、ゲート/root/imgpolicy/jsongate.sh <페이로드> [정책](プレースホルダーはペイロードとポリシーです)を作成してください(ポリシーの引数を省略すると/root/imgpolicy/policies/image-refs.yaml)。kyverno json scanの人が読む出力をパースして、違反1行ごとにFAIL <그 줄>(プレースホルダーはその行です)を出力し、最後にRESULT failed=<개수>(プレースホルダーは個数です)を出力した後、違反があれば終了コード1、なければ0で終了します。判定の行(PASSED・FAILED・ERROR:)が1つもなければ、通過とみなさずに、終了コード2で終了します。最後に、/root/imgpolicy/06-gate.txtを作成してください。1行目は、ポリシーをそのまま実行したときの終了コードを書いたSCAN EXIT <코드>(プレースホルダーはコードです)、その下に./jsongate.sh build.jsonの出力、最後の行はGATE EXIT <코드>(プレースホルダーはコードです)です。

スキャンは、KYVERNO_EXPERIMENTAL=true kyverno json scan --payload <JSON> --policy <YAML>です。環境変数を抜かすと、実験的機能なので、コマンド自体が拒否されます。このツールは、違反を見つけても終了コードが0で、--output jsonにも違反が記録されません。そのため、if kyverno json scan ...; thenで書いたゲートは、常に通過します。このステップで、自分で出力して確認するのが、それです。spec.rules[].assert.all[].checkのキーは、括弧で囲んだJMESPath式で、値が期待値です。JMESPathのフィルターで「入っていないもの」は、containsの結果をfalseと比べて選びます。JSONリテラルはバッククォートで囲むので、文字列も偽の値も、バッククォートが必要です。式だけを別にテストしてみるには、kyverno jp query -i <파일> '<식>'(プレースホルダーはファイルと式です)が最も速いです。アドミッションは最後の防衛線で、ここは人がPRで直せる場所です。同じルールを両側にかけておく理由が、それです。

固定は身元を決めるだけで、安全は決めない

ダイジェストが保証するのは、内容の同一性1つです。保証しない3つのことを、別の軸の仕組みが満たします。/root/imgpolicy/limits.tsvに、ちょうど3行を書いてください。各行は、タブ1つで区切られた2つのフィールドで、1つ目のフィールドはcontent-safety・author・build-pathの3つが1回ずつ、2つ目のフィールドは、その場所を満たす仕組みとして、scan・signature・provenanceのうち適切なもの1つです(1つ目のフィールドの順序は、この順序で書きます)。そして、/root/imgpolicy/revoked.txtに、取り消されたダイジェストを1行に1つずつ書いてください。今は1行だけです。registry.internal/sidecar:1.0が指すダイジェストが脆弱だと判明したので、その値を/root/imgpolicy/images.lockから探して、そのまま書きます(@の前の名前は書かず、sha256:で始まる値だけを書きます)。

同じダイジェストを2回取得すれば、同じバイト列です。それがすべてです。そのバイト列が安全かどうか、誰が作ったか、どのソースからどのパイプラインが作ったかは、ハッシュの中に入っていません。脆弱性検査は「内容が安全か」を、署名は「誰が保証するのか」を、来歴証明(SLSA provenance)は「どのように作られたのか」に答えます。3つの軸は、互いに代わりになれず、3つとも、何を指すかを、ダイジェストに頼ります。取り消しリストは、4つ目の軸です。固定をきちんとしておけば、「その1つを指名してブロックする」ことが可能になります。タブ文字は、printf 'a\tb\n'で入れるのが安全です。エディターがタブを空白に置き換えることが、よくあります。ロックファイルから値を取り出すには、jq -r '.["registry.internal/sidecar:1.0"]' /root/imgpolicy/images.lockを使ってください。

今ブロックに上げてもよいかを、リポジトリに答えさせる

/root/imgpolicy/audit.sh <매니페스트디렉터리> [폐기목록](プレースホルダーはマニフェストのディレクトリと取り消しリストです)を作成してください。取り消しリストの引数を省略すると、/root/imgpolicy/revoked.txtを使います。そのディレクトリの.yaml・.ymlからイメージ参照をすべて取り出し、ダイジェストで固定されていない参照ごとにFLOATING <파일이름> <참조>(プレースホルダーはファイル名と参照です)を、固定はされているがそのダイジェストが取り消しリストにあればREVOKED <파일이름> <참조>(プレースホルダーはファイル名と参照です)を、1行ずつ出力します(それらの行はソートして出力します)。最後の行は、正確にRESULT floating=<수> revoked=<수> ready=<yes|no>(プレースホルダーは数と、yesまたはnoです)で、両方とも0のときだけready=yesです。終了コードは、ready=yesなら0、そうでなければ1です。作成した後、./audit.sh manifestsの出力を/root/imgpolicy/audit.txtに保存し、そのファイルの末尾にEXIT <종료코드>(プレースホルダーは終了コードです)を1行追記してください。ステップ1で作ったマニフェストのまとまりが、そのまま対象です。

このステップが答える質問は、「ルールが正しいか」ではなく、「今オンにしてよいか」です。ポリシーを広げる前に、引っかかるものを先に数えるのが、ロールアウトの順序の2つ目のステップであり、その数字が0になる前にDenyに上げると、デプロイが止まって、ポリシーが元に戻されます。一度元に戻されたポリシーは、たいてい再びオンにされません。そのため、この数字を数える作業が、ルールを書く作業よりも重要なことが多いです。参照からダイジェストだけを取り除くには、${ref#*@}を使い、一覧と正確に1行が一致するかどうかは、grep -qxFで見ます。ゲートが常に0で終了すると、誰もそれを報告しません。きれいなまとまりと汚れたまとまりの、両方でテストしてみてください。