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

ポリシーをコードで

ポリシーを適用したのに何もブロックされなかった

TT Labで続きを見る

目標

Kubernetesに標準で入っているValidatingAdmissionPolicy(CEL)で、Podを検査するルールを作り、警告からブロックに上げ、メッセージに違反した値を入れ、例外と範囲とパラメーターまで、自分で付けてみます。

なぜ重要なのか

外部のポリシーエンジンをインストールしなくても、APIサーバーの中で検証できるようになったのが、ValidatingAdmissionPolicyです。Webhookとは違って、ネットワークの往復も証明書もなく、エンジンが落ちてクラスターが止まることもありません。その代わり、学ぶべきことが1つ増えます。判定ルールと適用範囲が、別々のオブジェクトであるという点です。この分離のおかげで、同じルールを、あるチームには警告として、あるチームにはブロックとして付けられ、ルールの値だけをConfigMapに出して、ポリシーを直さずに変更できます。逆に、この分離を知らないと、「ポリシーは確かに適用したのに、何もブロックされない」という場所で長く迷います。実務でポリシーをはじめて入れるときは、常にWarnから始めて、何が引っかかるかを見て、システムコンポーネントをmatchConditionsで除外しておいた後、範囲を狭く取ってDenyに上げます。この順序を飛ばしたポリシーは、デプロイを止めて元に戻され、そうなると、誰も二度とオンにしません。

ステップ

  1. /root/vapで作業します(export KUBECONFIG=/root/.kube/config、kubectl config use-context kwok-lab)。/root/vap/policy.yamlに、admissionregistration.k8s.io/v1のValidatingAdmissionPolicy require-cpu-limitを書いてください。matchConstraints.resourceRulesは、コアグループ("")のv1のpodsに対するCREATE・UPDATEを捉え、validationは、すべてのコンテナがresources.limits.cpuを持つことを要求します。テスト用のPodも2つ作成してください。/root/vap/pod-nocpu.yamlは、コンテナweb-tierがmemoryの制限だけを持ち、/root/vap/pod-ok.yamlは、コンテナweb-fullがcpuとmemoryの制限の両方を持ちます。ポリシーを適用した後、ラベルが1つもないネームスペースteam-openを作成し、そこにpod-nocpu.yamlをkubectl create --dry-run=serverで送って、その出力を/root/vap/01-nobinding.txtに保存してください。
  2. ネームスペースteam-warnを作成して、ラベルcpu-limit=warnを付けてください。/root/vap/binding-warn.yamlに、ValidatingAdmissionPolicyBinding cpu-limit-warnを書いてください。policyNameはrequire-cpu-limit、validationActionsは["Warn", "Audit"]、matchResources.namespaceSelector.matchLabelsはcpu-limit: warnです。適用した後、team-warnにpod-nocpu.yamlを--dry-run=serverで送って、出力を/root/vap/02-warn.txtに保存してください(標準エラー出力も一緒に)。警告が出るのに、Podは作られる必要があります。
  3. ネームスペースteam-denyを作成して、ラベルcpu-limit=denyを付けてください。/root/vap/binding-deny.yamlに、2つ目のバインディングcpu-limit-denyを書いてください。同じpolicyNameで、validationActionsは["Deny"]、セレクターはcpu-limit: denyです。適用した後、team-denyにpod-nocpu.yamlを送って、出力を/root/vap/03-deny.txtに保存し、続けてpod-ok.yamlも送って、通過するか確認してください。警告用のバインディング(cpu-limit-warn)は、削除せずにそのままにしておきます。2つの範囲が同時に生きているのが、実際のロールアウトの姿です。
  4. /root/vap/policy.yamlを修正して、spec.variablesにnoCpuを置いてください。cpuの制限がないコンテナの名前の一覧を計算します。validationはsize(variables.noCpu) == 0に変え、messageの代わりにmessageExpressionで、その名前とPodの名前を入れたメッセージを作ってください。そして、/root/vap/pod-two.yamlに、コンテナを2つ持つPod probe-twoを作成してください。web-nolimitはresources自体がなく、cache-okはcpuとmemoryの制限の両方を持ちます。ポリシーをもう一度適用した後、team-denyに送って、拒否メッセージを/root/vap/04-message.txtに保存してください。メッセージにはweb-nolimitだけが出てくる必要があり、cache-okは出てきてはいけません。
  5. /root/vap/policy.yamlに、spec.matchConditionsを追加してください。条件は2つです。skip-kube-system-saは、リクエスト元がsystem:serviceaccount:kube-system:で始まるサービスアカウントなら、ポリシーをスキップするようにし、skip-exempt-podsは、Podにラベルcpu-limit-exempt: "true"があればスキップするようにします。/root/vap/pod-exempt.yamlに、そのラベルが付いたPod probe-exempt(コンテナweb-tier、memoryの制限だけ)を作成してください。ポリシーをもう一度適用した後、2つのことをteam-denyで確認して、出力を/root/vap/05-skipped.txtに保存してください。(1)kubectl --as=system:serviceaccount:kube-system:replicaset-controller create -n team-deny -f pod-nocpu.yaml --dry-run=server、(2)pod-exempt.yamlを普段どおりに送ること。どちらも作られる必要があります。ラベルのないpod-nocpu.yamlは、依然として拒否される必要があります。
  6. ネームスペースvap-paramsとteam-images(ラベルregistry-check=on)を作成してください。vap-paramsにConfigMap allowed-registriesを作成し、キーallowedの値を、正確にregistry.internal/,ghcr.io/labhub/にしてください。/root/vap/policy-registries.yamlに、2つ目のポリシーallowed-registriesを書いてください。spec.paramKindはapiVersion: v1、kind: ConfigMapで、同じmatchConstraintsでPodを捉え、params.data['allowed']をカンマで分割した一覧のどれで始まるものでもないイメージを、拒否します。/root/vap/binding-registries.yamlに、バインディングallowed-registriesを書いてください。validationActionsは["Deny"]、paramRefはそのConfigMap(name・namespace、parameterNotFoundAction: Deny)、セレクターはregistry-check: "on"です。/root/vap/pod-img-bad.yamlに、イメージがdocker.io/nginx:1.27のPod probe-img-bad(コンテナweb-img、cpuとmemoryの制限を含む)を作成し、team-imagesにpod-ok.yamlとpod-img-bad.yamlを順に送って、2つの出力を/root/vap/06-param.txtに保存してください。
  7. /root/vap/scope.sh <네임스페이스>(プレースホルダーはネームスペースです)を作成してください。そのネームスペースに/root/vap/pod-nocpu.yamlを--dry-run=serverで送ってみて、拒否されたらDENYの1行と終了コード3、警告だけが出て作られたらWARNと2、何も言わずに作られたらOPENと0で終了します。出力は、その語の1行だけです。ネームスペースの名前やラベルを読んで判断せず、実際のリクエストの結果で判定してください。採点ツールは、team-openのラベルを一時的に変えながらこのスクリプトを呼び出し、元に戻します。作った後、team-deny・team-warn・team-openの3か所に順に実行して、結果を/root/vap/07-scope.txtに、<네임스페이스> <낱말>(プレースホルダーはネームスペースと語です)の形で、1行ずつ保存してください。
  8. /root/vap/policy.yamlに、2つ目のルールを追加してください。変数noMem(memoryの制限がないコンテナの名前の一覧)と、size(variables.noMem) == 0を検査する2つ目のvalidationです。このvalidationのmessageExpressionには、memoryという語とそのコンテナ名が入っている必要があり、1つ目のvalidationのメッセージには、cpuという語とそのコンテナ名が入っている必要があります。/root/vap/pod-mixed.yamlにPod probe-mixedを作成してください。コンテナweb-tierはmemoryの制限だけを、cache-tierはcpuの制限だけを持ちます。ポリシーをもう一度適用した後、このPodをteam-warnとteam-denyに順に送って、2つの出力を/root/vap/08-two-rules.txtに保存してください。Warn側は2つのルールの両方を知らせ、Deny側は1つだけを知らせます。

参考

ポリシーだけを適用しても、何も起きない

/root/vapで作業します(export KUBECONFIG=/root/.kube/config、kubectl config use-context kwok-lab)。/root/vap/policy.yamlに、admissionregistration.k8s.io/v1のValidatingAdmissionPolicy require-cpu-limitを書いてください。matchConstraints.resourceRulesは、コアグループ("")のv1のpodsに対するCREATE・UPDATEを捉え、validationは、すべてのコンテナがresources.limits.cpuを持つことを要求します。テスト用のPodも2つ作成してください。/root/vap/pod-nocpu.yamlは、コンテナweb-tierがmemoryの制限だけを持ち、/root/vap/pod-ok.yamlは、コンテナweb-fullがcpuとmemoryの制限の両方を持ちます。ポリシーを適用した後、ラベルが1つもないネームスペースteam-openを作成し、そこにpod-nocpu.yamlをkubectl create --dry-run=serverで送って、その出力を/root/vap/01-nobinding.txtに保存してください。

ポリシーは「判定する方法」だけを定義し、「どこに適用するか」は、別のバインディングオブジェクトが決めます。そのため、ポリシーだけを適用すると、APIサーバーはその式を評価すらしません。CELでリスト全体を検査するときはall(...)を使い、ないかもしれないフィールドは、has(...)で先に確認する必要があります。kubectl create -f <파일> --dry-run=server(プレースホルダーはファイルです)は、アドミッションをそのまま通しつつ、オブジェクトは残しません。拒否メッセージも警告も、本物のリクエストとまったく同じように出ます。

バインディングを付けると、警告が出はじめる

ネームスペースteam-warnを作成して、ラベルcpu-limit=warnを付けてください。/root/vap/binding-warn.yamlに、ValidatingAdmissionPolicyBinding cpu-limit-warnを書いてください。policyNameはrequire-cpu-limit、validationActionsは["Warn", "Audit"]、matchResources.namespaceSelector.matchLabelsはcpu-limit: warnです。適用した後、team-warnにpod-nocpu.yamlを--dry-run=serverで送って、出力を/root/vap/02-warn.txtに保存してください(標準エラー出力も一緒に)。警告が出るのに、Podは作られる必要があります。

バインディングは、「このポリシーをどこに、どの強度で」を決めます。Warnはレスポンスのヘッダーで警告だけを返してリクエストを通過させ、Auditは監査ログに注釈を残します。警告は標準出力ではなく標準エラー出力に出るので、2>&1を付けないと、ファイルに一緒に入りません。namespaceSelectorは、ネームスペースオブジェクトのラベルを見ます。

同じポリシーをDenyに上げると、リクエストがブロックされる

ネームスペースteam-denyを作成して、ラベルcpu-limit=denyを付けてください。/root/vap/binding-deny.yamlに、2つ目のバインディングcpu-limit-denyを書いてください。同じpolicyNameで、validationActionsは["Deny"]、セレクターはcpu-limit: denyです。適用した後、team-denyにpod-nocpu.yamlを送って、出力を/root/vap/03-deny.txtに保存し、続けてpod-ok.yamlも送って、通過するか確認してください。警告用のバインディング(cpu-limit-warn)は、削除せずにそのままにしておきます。2つの範囲が同時に生きているのが、実際のロールアウトの姿です。

ポリシー1つに、バインディングを複数付けられ、各バインディングが、自分の範囲と自分の強度を持ちます。そのため、「同じルールを、あるチームには警告として、あるチームにはブロックとして」が、ポリシーをコピーせずにできます。拒否メッセージには、どのポリシーとどのバインディングがブロックしたのかが一緒に出力されます。複数のルールが重なるときに、原因を探す手がかりです。

拒否メッセージが、どのコンテナかを教えてくれる

/root/vap/policy.yamlを修正して、spec.variablesにnoCpuを置いてください。cpuの制限がないコンテナの名前の一覧を計算します。validationはsize(variables.noCpu) == 0に変え、messageの代わりにmessageExpressionで、その名前とPodの名前を入れたメッセージを作ってください。そして、/root/vap/pod-two.yamlに、コンテナを2つ持つPod probe-twoを作成してください。web-nolimitはresources自体がなく、cache-okはcpuとmemoryの制限の両方を持ちます。ポリシーをもう一度適用した後、team-denyに送って、拒否メッセージを/root/vap/04-message.txtに保存してください。メッセージにはweb-nolimitだけが出てくる必要があり、cache-okは出てきてはいけません。

variablesは、式1つに名前を付けておいて、variables.<이름>(プレースホルダーは名前です)で再利用する仕組みです。遅延評価なので、validationで使わなければ計算されません。filter(...)で違反したものだけを残して、map(...)で名前だけを取り出せば、リストになります。文字列のリストは、join(', ')でつなげられます。messageExpressionがエラーを出すと、APIサーバーは黙ってmessageに戻ります。メッセージが変わらなければ、式を疑ってください。

ポリシーがそもそも見ないリクエストを作る

/root/vap/policy.yamlに、spec.matchConditionsを追加してください。条件は2つです。skip-kube-system-saは、リクエスト元がsystem:serviceaccount:kube-system:で始まるサービスアカウントなら、ポリシーをスキップするようにし、skip-exempt-podsは、Podにラベルcpu-limit-exempt: "true"があればスキップするようにします。/root/vap/pod-exempt.yamlに、そのラベルが付いたPod probe-exempt(コンテナweb-tier、memoryの制限だけ)を作成してください。ポリシーをもう一度適用した後、2つのことをteam-denyで確認して、出力を/root/vap/05-skipped.txtに保存してください。(1)kubectl --as=system:serviceaccount:kube-system:replicaset-controller create -n team-deny -f pod-nocpu.yaml --dry-run=server、(2)pod-exempt.yamlを普段どおりに送ること。どちらも作られる必要があります。ラベルのないpod-nocpu.yamlは、依然として拒否される必要があります。

matchConditionsは、matchConstraintsが選んだリクエストを、もう一度ふるいにかけます。条件がすべて真のときにだけvalidationsが評価されるので、「スキップしたい」という条件を否定で書きます。requestには、リクエスト元の情報(request.userInfo.username)が入っています。コントローラーが作るPodまでブロックすると、クラスターが自分自身を復旧できなくなるので、システムのサブジェクトを除外しておくのが、ロールアウトの最初の安全装置です。kubectl --asで、別のサブジェクトのふりができます。

ルールの値をConfigMapに出して、ポリシーを直さずに変更する

ネームスペースvap-paramsとteam-images(ラベルregistry-check=on)を作成してください。vap-paramsにConfigMap allowed-registriesを作成し、キーallowedの値を、正確にregistry.internal/,ghcr.io/labhub/にしてください。/root/vap/policy-registries.yamlに、2つ目のポリシーallowed-registriesを書いてください。spec.paramKindはapiVersion: v1、kind: ConfigMapで、同じmatchConstraintsでPodを捉え、params.data['allowed']をカンマで分割した一覧のどれで始まるものでもないイメージを、拒否します。/root/vap/binding-registries.yamlに、バインディングallowed-registriesを書いてください。validationActionsは["Deny"]、paramRefはそのConfigMap(name・namespace、parameterNotFoundAction: Deny)、セレクターはregistry-check: "on"です。/root/vap/pod-img-bad.yamlに、イメージがdocker.io/nginx:1.27のPod probe-img-bad(コンテナweb-img、cpuとmemoryの制限を含む)を作成し、team-imagesにpod-ok.yamlとpod-img-bad.yamlを順に送って、2つの出力を/root/vap/06-param.txtに保存してください。

paramKindは「このポリシーがパラメーターとして何を読むか」を、バインディングのparamRefは「そのうちのどのオブジェクトか」を決めます。そのため、同じポリシーを、チームごとに異なる値で付けられます。CELの文字列にはsplitとstartsWithがあり、リストにはexistsがあります。パラメーターが見つからなかったときにどうするかは、parameterNotFoundActionが決めます。セキュリティのルールなら、開ける側ではなく、ブロックする側がデフォルトであるべきです。このラボの後半で、採点ツールがConfigMapの値を一時的に変えてみて、元に戻します。許可リストを式の中に書いておくと、そのときに露見します。

同じリクエストが、ネームスペースごとに異なる答えを受け取る

/root/vap/scope.sh <네임스페이스>(プレースホルダーはネームスペースです)を作成してください。そのネームスペースに/root/vap/pod-nocpu.yamlを--dry-run=serverで送ってみて、拒否されたらDENYの1行と終了コード3、警告だけが出て作られたらWARNと2、何も言わずに作られたらOPENと0で終了します。出力は、その語の1行だけです。ネームスペースの名前やラベルを読んで判断せず、実際のリクエストの結果で判定してください。採点ツールは、team-openのラベルを一時的に変えながらこのスクリプトを呼び出し、元に戻します。作った後、team-deny・team-warn・team-openの3か所に順に実行して、結果を/root/vap/07-scope.txtに、<네임스페이스> <낱말>(プレースホルダーはネームスペースと語です)の形で、1行ずつ保存してください。

警告は標準エラー出力に出るので、出力をまとめて受け取らないと見えません。拒否は終了コードが0ではなく、警告は終了コードが0です。この2つを先に分けてから、警告かどうかを見ます。set -eをかけると、拒否の時点でスクリプトが先に死にます。アドミッションは、ネームスペースのラベルを見て範囲を決めるので、同じマニフェストが、場所によって異なる答えを受け取ります。

ルール2つを1つのポリシーに入れると、DenyとWarnが異なるものを知らせる

/root/vap/policy.yamlに、2つ目のルールを追加してください。変数noMem(memoryの制限がないコンテナの名前の一覧)と、size(variables.noMem) == 0を検査する2つ目のvalidationです。このvalidationのmessageExpressionには、memoryという語とそのコンテナ名が入っている必要があり、1つ目のvalidationのメッセージには、cpuという語とそのコンテナ名が入っている必要があります。/root/vap/pod-mixed.yamlにPod probe-mixedを作成してください。コンテナweb-tierはmemoryの制限だけを、cache-tierはcpuの制限だけを持ちます。ポリシーをもう一度適用した後、このPodをteam-warnとteam-denyに順に送って、2つの出力を/root/vap/08-two-rules.txtに保存してください。Warn側は2つのルールの両方を知らせ、Deny側は1つだけを知らせます。

validationsはリストで、各項目が、自分のexpressionと自分のmessageExpressionを持ちます。変数は、複数のvalidationが分け合って使います。Denyは最初に失敗した項目でリクエストを終わらせますが、Warnは失敗したものをすべて警告として返します。そのため、ロールアウト初期のWarnの段階が、「何をすべて直す必要があるか」を一度に見せてくれます。2つのメッセージが区別できなければ、どのルールが引っかかったのか、誰にもわかりません。