四つのアンカーと、ポリシーを割らずに段階導入する方法
一言でいうと
validateルールには3つの文法があります。オブジェクトの形をそのまま描くpattern、条件式で拒否するdeny.conditions、そしてCEL式です。これに、強制レベルをルール単位で決めるfailureActionが加わったことで、ポリシーを2つに分割しなくても、新しいルールだけを観察モードで上乗せする流れが可能になりました。
なぜ必要なのか
新しいポリシー言語を覚えさせないというのが、Kyvernoの出発点です。OPA/GatekeeperはRegoを習得しなければならず、ConstraintTemplateとConstraintの2つのオブジェクトを組み合わせる必要があります。Kyvernoは、検査したいオブジェクトをそのままYAMLで描き、値の位置に演算子を入れます。Podのコンテナにメモリリミットが必要なら、Podスペックの形のまま書いて、値の位置に「空でない」ことを表す演算子を入れる、という具合です。
ただし、形をそのまま描く方式には限界があります。「このフィールドがあればあの条件も見なければならない」や「このキーはそもそも存在してはいけない」といった関係は、値では表現できません。そこで、キー名に付けるアンカーが生まれました。
どう動くのか
値の位置の演算子は次のとおりです。?*は空でない値、*はnullを含むすべての値、X|Yはどちらか一方、!XはXではない値を表し、>=256Miのように数値の比較もできます。
キーの位置のアンカーは4種類で、ここで最も事故が起きます。
| アンカー | 名前 | 意味 |
|---|---|---|
() |
条件アンカー | このキーがこの値と一致するときだけ、残りを検査します |
=() |
等価アンカー | このキーが存在する場合、値が条件を満たす必要があります |
^() |
追加アンカー | 配列の中に、条件を満たす要素が最低1つ必要です |
X() |
否定アンカー | このキーが存在してはいけません |
最もよくある誤用が否定アンカーです。X(privileged)を「privilegedの値が違わなければならない」と読む人が多いのですが、正確には「privilegedというキー自体が存在してはいけない」という意味です。値がfalseと明示されていても、キーがあれば引っかかります。値を比較したいなら、アンカーではなく値の演算子かdenyの条件を使う必要があります。
foreachはコレクションの各要素を検査します。ここにも落とし穴が1つあり、foreach.listはJMESPath式そのものを受け取るので、波括弧で囲んではいけません。そして、initContainersまで一緒に見たいときに使う定型句がrequest.object.spec.[initContainers, containers][]です。initContainersを見落としたポリシーは実際に非常に多く、攻撃者ではなく、ただ初期化コンテナを使う普通のチームがポリシーを回避することになります。
deny.conditionsは、条件が真のときに拒否します。allとanyで束ね、各条件はkey、operator、valueの3つの欄からなります。patternでは表現しづらいもののほとんどが、ここに来ます。CELはKubernetes 1.25以降で標準となった式言語で、Kyvernoにもvalidate.celとして取り入れられています。ただし、これはすでにKyvernoを運用しているときに便利な選択肢であって、Kyvernoを導入する理由にはなりません。フィールド1つを検査するだけなら、Kubernetes組み込みのValidatingAdmissionPolicyで十分で、コンポーネントを1つ少なく運用できるということは、アップグレード対象が1つ減り、Webhook証明書の更新を気にする場面が1つ減るということです。
最後が強制レベルです。以前はspec.validationFailureActionがポリシー全体の強制レベルを決めていました。そのため、1つのポリシーの中で、あるルールは確信があるので止めたいが、別のルールはまだ観察だけにしたい場合は、ポリシーを2つに分割しなければならず、matchブロックが複製されて管理対象が増えました。現在はspec.rules[*].validate[*].failureActionに下りてきており、値はEnforceとAuditの2つです。指定しなければ、デフォルトはAuditです。新しいルールを既存のポリシーにAuditで上乗せし、数日間レポートを見てから、そのルールだけをEnforceに上げる流れが、ポリシーを分割せずに可能になったことが、この変更の価値です。一緒に動いたフィールドにwebhookTimeoutSecondsとfailurePolicyがあり、それぞれwebhookConfiguration.timeoutSeconds、webhookConfiguration.failurePolicyへ移され、1.13からdeprecatedと表示されます。
現場での姿
著者のイメージスキャンの経験が、このテーマにぴったり重なります。あるイメージを初めてスキャンしたとき、全部で1,247件が出て、そのうちCriticalが9件でした。このレポートをそのままチームのチャンネルに貼ると、結果は2つのどちらかです。全員が無視するか、誰も手を付けられないままリリースが止まるかです。修正版があるものだけを残すオプションを1つ付けたところ4件になり、実際に悪用が確認されたリストと突き合わせると、今日対処すべきものは1件でした。
ポリシーもまったく同じです。最初からEnforceで全面適用すると、デプロイがまるごと止まり、結局、ボトルネックになるのはポリシーではなくポリシーを作った人です。かといって、すべてをAuditにしておくと、誰もレポートを見ません。答えは、ルール単位のfailureActionです。確実なルールをいくつかだけEnforceに上げておき、残りはAuditで観察すれば、今対処できる項目だけが人の目の前に残ります。スキャンレポートで学んだことと同じ原理です。
次のラボですること
/root/kca-validate/にClusterPolicyを書きながら、match・pattern・deny・excludeを1つずつ埋め、ルールごとに異なるfailureActionを指定します。そして、同じルールをKubernetes組み込みのValidatingAdmissionPolicyとBindingで実際のクラスターに載せ、ポリシーエンジンなしでどこまでできるかを目で見て比較します。