Deployment は作成されたのに Pod が 1 つもない
目標
Kyvernoポリシーの記述ツール(preconditions、apiCallコンテキスト、変数のデフォルト値、autogen、JSONパッチ、foreach、CEL、バックグラウンドスキャン)が、実際のリクエストで何を拒否し、何を書き換えるのかを、本物のKyverno 1.19.1で確認します。
なぜ重要なのか
ポリシーの文法が正しいことと、意図どおりに判定することは、別のものです。matchはリソースの種類と場所だけで選ぶので、リクエストの内容に応じてルールをオン・オフするにはpreconditionsが、リクエストにない情報が必要ならコンテキストが必要です。変数が解釈されないと、ルールは判定の代わりにエラーを出し、Enforceではそのエラーが拒否として見えるので、原因を見当違いの場所に探すことになります。Podルールは、デフォルトでコントローラー用のルールとして自動生成されますが、これを切ると、Deploymentは成功したように見えるのに、Podだけが静かに作られません。書き換えルールは、リクエストごとに違う値を入れたり、リストの一部だけを直したりできなければならず、すでにクラスターにあったリソースは、アドミッションを再び通らないので、バックグラウンドスキャンでしか明らかになりません。
ステップ
- ネームスペース
kca-preを作成し、/root/kca-write/01-limits.yamlにClusterPolicylimits-for-prod(background false)を書いて適用してください。ルールmemory-limitはkca-preのPodをmatchし、preconditionsで、ラベルtierがprodのリクエストにだけ適用され(ラベルがなければ空文字列として扱います)、すべてのコンテナにresources.limits.memoryを要求します(Enforce)。matchにはラベルセレクターを使いません。 - ネームスペース
kca-prod(ラベルenv=prod)とkca-dev(ラベルenv=dev)を作成し、/root/kca-write/02-team.yamlにClusterPolicyteam-in-prod-ns(background false)を適用してください。ルールteam-when-prodは、2つのネームスペースのPodについて、コンテキストnsenvをapiCallで/api/v1/namespaces/{{request.namespace}}から読み取り(ラベルenv、なければnone)、envがprodでteamラベルがないときに拒否します(Enforce)。そのあと、kca-devのenvを一時的にprodに変えて、ラベルのないPodをサーバーdry-runで送った出力全体を/root/kca-write/flip.txtに保存し、envをdevに戻してください。 - ネームスペース
kca-varに、ClusterPolicyno-platform-team(background false、ルールnot-platform)を適用してください。teamラベルがplatformなら拒否するdeny条件ですが、最初はkeyを、デフォルト値なしで{{ request.object.metadata.labels.team }}と書きます。ラベルのないPodをサーバーdry-runで送り、拒否の出力全体を/root/kca-write/missing.txtに保存してください。そのあと、/root/kca-write/03-no-platform.yamlのkeyにデフォルト値(空文字列)を加えて再び適用し、ラベルのないPodは受け入れ、team=platformは拒否するようにしてください。 - ネームスペース
kca-autoとkca-noautoを作成し、各ネームスペースのPodにreadinessProbeを要求する(Enforce、background false、ルールneed-probe)ClusterPolicyprobes-autoとprobes-noautoを、/root/kca-write/04-probes.yamlに書いて適用してください。probes-noautoにだけ、アノテーションpod-policies.kyverno.io/autogen-controllers: noneを付けます。そのあと、2つのネームスペースでkubectl create deployment web --image=nginx:1.27-alpineを実行し、kca-auto側の出力全体を/root/kca-write/auto.txtに保存してください。 - ネームスペース
kca-mutを作成し、/root/kca-write/05-requested-by.yamlにClusterPolicyrequested-by(ルールannotate-user)を書いてください。kca-mutのPodに、patchesJson6902のadd操作でアノテーションkca.io/requested-byを入れ、値は{{request.userInfo.username}}です。まずspec.backgroundを書かずに適用して、拒否される出力全体を/root/kca-write/bg-denied.txtに保存したあと、backgroundをfalseにして適用してください。既存のアノテーションは保存されている必要があります。 /root/kca-write/06-pull-policy.yamlにClusterPolicypull-policy(ルールlatest-always)を書いて適用してください。kca-mutのPodについて、foreachでコンテナごとにイメージが:latestで終わるかを要素単位のpreconditionsで見て、そのコンテナだけをpatchStrategicMergeでimagePullPolicy: Alwaysに変えます。タグを固定したコンテナのimagePullPolicyは、そのままである必要があります。- ネームスペース
kca-celを作成し、/root/kca-write/07-no-priv-esc.yamlに、policies.kyverno.io/v1のValidatingPolicyno-priv-escを適用してください。validationActionsは[Deny]、matchConstraintsはcore v1 podsのCREATE・UPDATEと、namespaceSelectorkubernetes.io/metadata.name: kca-celです。CEL変数escalatingに、securityContext.allowPrivilegeEscalationが明示的にfalseではないコンテナ名のリストを入れ、そのリストが空であれば通過し、拒否メッセージ(messageExpression)には、その名前をカンマでつないで表示します。 - ネームスペース
kca-bgに、ラベルのないPodold-nolabelと、ラベルteam=aのPodold-labelを先に作成したあと、/root/kca-write/08-audit-team.yamlにClusterPolicyaudit-team(background true、Audit、ルールteam、kca-bgのPodにteamラベルを要求)を適用してください。PolicyReportに2つのPodの結果が現れるまで待ってから、/root/kca-write/report.jsonに、fail・passキーで、それぞれの結果のPod名のリストを書きます。
参考
- VMの中に、k3sとKyverno v1.19.1があります。このバージョンでは、
kyverno.io/v1 ClusterPolicyは使用中止の警告を出しますが、動作します。 - 保存せずにポリシーをテストするには、
kubectl -n <ns> run t --image=nginx:1.27-alpine --dry-run=serverを使います(書き換えの結果は-o yamlで見ます)。 - ポリシーの準備確認は、
kubectl get clusterpolicy <이름> -o jsonpath='{.status.conditionStatus.ready}'(プレースホルダーは名前です)で、ValidatingPolicyは.status.conditionStatus.readyです。 - よくある間違い: ポリシーを適用した直後に、すぐリクエストを送ること。Webhookへの反映に数秒かかります。
- よくある間違い: 1つのネームスペースに、複数のステップのポリシーを重ねて置くこと。このラボは、ステップごとにネームスペースを分けています。
- Preconditions・External Data Sources(apiCall)・Variables・Auto-Gen Rules・Mutate Rules・ValidatingPolicy・Reporting
prodのPodだけにlimitを要求する
ネームスペースkca-preを作成し、/root/kca-write/01-limits.yamlにClusterPolicy limits-for-prod(background false)を書いて適用してください。ルールmemory-limitはkca-preのPodをmatchし、preconditionsで、ラベルtierがprodのリクエストにだけ適用され(ラベルがなければ空文字列として扱います)、すべてのコンテナにresources.limits.memoryを要求します(Enforce)。matchにはラベルセレクターを使いません。
preconditionsのkeyは、JMESPath変数です。ラベルのないリクエストで変数が解釈されないとどうなるかは、ステップ3で見ます。採点ツールは、tierがない・dev・prodのPodを、サーバーdry-runで送ってみます。
ネームスペースのラベル1つでルールがオンになる
ネームスペースkca-prod(ラベルenv=prod)とkca-dev(ラベルenv=dev)を作成し、/root/kca-write/02-team.yamlにClusterPolicy team-in-prod-ns(background false)を適用してください。ルールteam-when-prodは、2つのネームスペースのPodについて、コンテキストnsenvをapiCallで/api/v1/namespaces/{{request.namespace}}から読み取り(ラベルenv、なければnone)、envがprodでteamラベルがないときに拒否します(Enforce)。そのあと、kca-devのenvを一時的にprodに変えて、ラベルのないPodをサーバーdry-runで送った出力全体を/root/kca-write/flip.txtに保存し、envをdevに戻してください。
ネームスペース名をポリシーに埋め込むと、ポリシーを直さなければ判定が変わりません。apiCallは、リクエストごとにAPIサーバーの現在の値を読み取ります。dry-runは、kubectl run ... --dry-run=serverです。
ラベルのないPodが、見当違いの理由で拒否された
ネームスペースkca-varに、ClusterPolicy no-platform-team(background false、ルールnot-platform)を適用してください。teamラベルがplatformなら拒否するdeny条件ですが、最初はkeyを、デフォルト値なしで{{ request.object.metadata.labels.team }}と書きます。ラベルのないPodをサーバーdry-runで送り、拒否の出力全体を/root/kca-write/missing.txtに保存してください。そのあと、/root/kca-write/03-no-platform.yamlのkeyにデフォルト値(空文字列)を加えて再び適用し、ラベルのないPodは受け入れ、team=platformは拒否するようにしてください。
JMESPathで存在しないキーを読むと、変数の置換が失敗し、Kyvernoはそのルールを、判定できなかったエラーとして扱います。Enforceルールのエラーがリクエストをどうするかを、出力の文言を読んで確認してください。||演算子が、デフォルト値を与えます。
Deploymentは作られたのに、Podが1つもない
ネームスペースkca-autoとkca-noautoを作成し、各ネームスペースのPodにreadinessProbeを要求する(Enforce、background false、ルールneed-probe)ClusterPolicy probes-autoとprobes-noautoを、/root/kca-write/04-probes.yamlに書いて適用してください。probes-noautoにだけ、アノテーションpod-policies.kyverno.io/autogen-controllers: noneを付けます。そのあと、2つのネームスペースでkubectl create deployment web --image=nginx:1.27-alpineを実行し、kca-auto側の出力全体を/root/kca-write/auto.txtに保存してください。
Kyvernoは、PodルールをDeployment・ReplicaSetなど、Podテンプレートを持つリソース用のルールに自動的に拡張します。拡張しなければ、Deploymentは通過し、拒否はReplicaSetコントローラーがPodを作るときに起きます。kubectl describe rsの条件とイベントを見てください。
リクエストした人の名前をPodに刻む
ネームスペースkca-mutを作成し、/root/kca-write/05-requested-by.yamlにClusterPolicy requested-by(ルールannotate-user)を書いてください。kca-mutのPodに、patchesJson6902のadd操作でアノテーションkca.io/requested-byを入れ、値は{{request.userInfo.username}}です。まずspec.backgroundを書かずに適用して、拒否される出力全体を/root/kca-write/bg-denied.txtに保存したあと、backgroundをfalseにして適用してください。既存のアノテーションは保存されている必要があります。
JSON Pointerでは、/を~1と書きます。リクエスターの情報は、アドミッションのリクエストにしかなく、すでに保存されているリソースをもう一度走査するバックグラウンド処理にはありません。サーバーdry-runに-o jsonpathを付けると、書き換えられた結果を見られます。
latestタグのコンテナだけを毎回新しく取得させる
/root/kca-write/06-pull-policy.yamlにClusterPolicy pull-policy(ルールlatest-always)を書いて適用してください。kca-mutのPodについて、foreachでコンテナごとにイメージが:latestで終わるかを要素単位のpreconditionsで見て、そのコンテナだけをpatchStrategicMergeでimagePullPolicy: Alwaysに変えます。タグを固定したコンテナのimagePullPolicyは、そのままである必要があります。
foreachの中で、現在の要素はelementです。リストから特定のコンテナだけを直すには、条件アンカー(name)で名前を合わせます。JMESPathには、ends_with関数があります。
CELの1行が、Deploymentまで止めた
ネームスペースkca-celを作成し、/root/kca-write/07-no-priv-esc.yamlに、policies.kyverno.io/v1のValidatingPolicy no-priv-escを適用してください。validationActionsは[Deny]、matchConstraintsはcore v1 podsのCREATE・UPDATEと、namespaceSelector kubernetes.io/metadata.name: kca-celです。CEL変数escalatingに、securityContext.allowPrivilegeEscalationが明示的にfalseではないコンテナ名のリストを入れ、そのリストが空であれば通過し、拒否メッセージ(messageExpression)には、その名前をカンマでつないで表示します。
CELのfilter・mapでリストを作り、has()で存在しないフィールドを先に確認します。このバージョンのValidatingPolicyも、Podルールを、Podテンプレートを持つリソースに自動生成します(status.autogen)。採点ツールは、Deploymentもdry-runで送ってみます。
ポリシーより先にあったPodも、レポートに載った
ネームスペースkca-bgに、ラベルのないPod old-nolabelと、ラベルteam=aのPod old-labelを先に作成したあと、/root/kca-write/08-audit-team.yamlにClusterPolicy audit-team(background true、Audit、ルールteam、kca-bgのPodにteamラベルを要求)を適用してください。PolicyReportに2つのPodの結果が現れるまで待ってから、/root/kca-write/report.jsonに、fail・passキーで、それぞれの結果のPod名のリストを書きます。
Auditは、リクエストを止めずに記録だけします。すでに保存されているリソースは、アドミッションを再び通らないので、backgroundがあってはじめて評価されます。結果は、ネームスペースのPolicyReport(リソースごとに1つ)に集まります。