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

ポリシーをコードで

Pod Security Standards と Admission — レベルとモードは別の軸

TT Labで続きを見る

一言でいうと

Pod Security Standardsは、何を禁止するか(privileged・baseline・restricted)を決め、Pod Security Admissionは、違反をどう扱うか(enforce・audit・warn)を決めます。この2つは別々の軸なので、ネームスペースのラベルで自由に組み合わせられます。

なぜ必要なのか

Podスペックには、ホストをまるごと開くスイッチがいくつもあります。hostNetwork、hostPID、privileged: true、hostPathボリューム、任意のcapabilityの追加です。1つでもオンにすると、コンテナの分離が事実上なくなります。ところが、これらのフィールドは、正常なインフラのワークロード(CNIエージェント、ログコレクター、ノードエクスポーター)が、実際に使うものでもあります。「禁止」ではなく「どこまで許可するか」を決めなければならない問題なので、単純な禁止リストでは解決しませんでした。

PodSecurityPolicyがその役目を担っていましたが、v1.25で削除されました。PSPは、ユーザーではなくPodを作る主体(たいていコントローラーのサービスアカウント)の権限を見ており、Podに複数のPSPがかかるとき、どれが適用されるかを予測しにくかったのです。代わりに入ったのが、Pod Security Admissionです。ルールはプロジェクトが決めて、3つのレベルに固定し、適用の単位は、ネームスペースのラベル1つに単純化しました。

どう動くのか

1つ目の軸は、レベル(Pod Security Standards)です。

レベル 性格
privileged 制約なし。システム・インフラのワークロード向け
baseline 既知の権限昇格を防ぐ最小限の制約。デフォルトのPodスペックはそのまま通過する
restricted Podのハードニングのベストプラクティスを強く要求する

baselineがブロックするのは、ホストのネームスペースの共有、privileged、許可リスト外のcapability、hostPathボリューム、ホストポート、AppArmor・SELinux・seccompの回避のようなものです。restrictedは、ここにボリュームの種類の制限、allowPrivilegeEscalation: false、runAsNonRoot: true、runAsUserを0にしないこと、seccompプロファイルの明示的な指定、すべてのcapabilityをdropしてNET_BIND_SERVICEだけを許可することを追加します。ここで、重要な違いがあります。baselineは、「変わったものを使わなければ」通過しますが、restrictedは、ごく普通のマニフェストも通過できません。何も書いていないコンテナは、seccompプロファイルがなく、capabilityをdropしていないからです。

2つ目の軸は、モード(Pod Security Admission)です。

モード 違反のとき
enforce Podを拒否する
audit 監査ログのイベントにアノテーションを追加して、通過させる
warn ユーザーに警告を見せて、通過させる

2つの軸は、ラベルで出会います。モードごとに、ラベルが2つずつあります。

apiVersion: v1
kind: Namespace
metadata:
  name: my-baseline-namespace
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/enforce-version: v1.30
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

この組み合わせが、導入手順そのものです。今はbaselineだけを強制し、restricted違反は警告と監査で数えてみます。1つのネームスペースが、3つのモードすべてを設定でき、モードごとに異なるレベルを与えられるので、「次の目標を先にオンにして観察する」ことが、ラベル2行でできます。

-versionの接尾辞を抜かすと、アップグレードの日にデプロイが止まります。バージョンのラベルの値は、有効なKubernetesのマイナーバージョンかlatestで、書かなければlatestとして動作します。レベルの内容は、バージョンごとに変わります。実際に、v1.25でrestrictedが変わりました。latestにしておくと、クラスターを上げた日に、マニフェストを何も変えていないのに、昨日まで通っていたデプロイがブロックされます。逆に、バージョンを固定しておけば、レベルが強くなっても、その日は何も起きず、バージョンを上げる日程は、人が決めます。ポリシーの変更とクラスターのアップグレードを、分離する仕組みです。

アドミッションはリクエストを見る仕組みです。すでに起動しているPodは、ブロックできません。ラベルを付けた後も、違反しているPodは動き続け、それが再起動されたり、再びスケジュールされたりするときにはじめて、拒否されます。そのため、切り替えの前に、違反を先に取り出してみる手順が必要です。ラベルのコマンドをサーバーのdry-runで実行すると、そのネームスペースにすでにあるPodの違反が、警告として出ます。

kubectl label --dry-run=server --overwrite ns payments \
  pod-security.kubernetes.io/enforce=restricted

ドキュメントに書かれている、見落としやすい事実がもう1つあります。enforceはワークロードのリソースには適用されず、その結果として作られるPodにだけ適用されます。auditとwarnは、Deploymentのようなワークロードにも適用されます。そのため、違反するテンプレートを持つDeploymentは、kubectl applyが成功し(警告は出ます)、Podが作られないことは、数秒後のReplicaSetのイベントではじめて表に出ます。「デプロイはされたのにPodがない」という問い合わせの、よくある原因です。

適用除外(exemption)は、ラベルではなく、アドミッションコントローラーの設定ファイルに書き、軸は3つあります。ユーザー名、RuntimeClass名、ネームスペースです。ドキュメントは、コントローラーのサービスアカウントを適用除外しないようにと警告しています。replicaset-controllerを適用除外すると、ワークロードを作れるすべての人が、暗黙的に適用除外されます。

現場での姿

1つ目は、restrictedをいきなりenforceでオンにしたチームです。ごく普通のマニフェストが、すべてブロックされます。securityContextを一度も書いたことのないチームにとっては、「すべてのDeploymentを直せ」という意味なので、たいてい1日でラベルが下げられます。順序は、warn・auditでrestrictedをオンにしておき、enforceはbaselineに置くことです。

2つ目は、システムのネームスペースにrestrictedをかけた場合です。CNIエージェントやノードエクスポーターが、再起動される瞬間に起動できません。クラスターが自分自身を復旧できない状態になりますが、原因がラベルだとわかるのは、Podのイベントを開いてみてからです。

3つ目は、クラスターのアップグレードの日の障害です。-versionなしでオンにしておいたネームスペースでだけ、デプロイがブロックされます。マニフェストもポリシーも変わっていないので、原因を探すのに時間がかかります。バージョンの固定は怠慢ではなく、変更管理です。

4つ目は、メトリクスで見る方法です。kube-apiserverが、pod_security_evaluations_total、pod_security_errors_total、pod_security_exemptions_totalを出力します。auditモードでオンにしておいた期間に、これらの値をダッシュボードに載せておけば、「enforceに上げてもよいか」に、数字で答えられます。

参考ドキュメント

次のラボですること

kwokクラスターのネームスペースに、ラベルを直接付けながら、切り替えの手順を一周します。違反するPodを先に起動しておき、--dry-run=serverでラベルを試して、既存の違反が警告として出るのを確認し、warnとauditだけをオンにした状態で、同じPodが通過するのを見た後、enforceに上げて拒否されるのを確認します。-versionを固定したネームスペースと、latestのネームスペースを並べて置き、違反するテンプレートを持つDeploymentが、applyには成功しながらPodが作られない場面も、自分で作ります。