PSPはなぜ死に、PSAは何が違うのか
一言でいうと
PSPは「ポリシーとユーザーをRBACで結び付ける」構造でしたが、それがあまりにも難しいものでした。 PSAはその結び付けを捨て、ネームスペースのラベル3つに単純化しました。表現力を失う代わりに 運用しやすさを得た、トレードオフです。
なぜ必要なのか
PodSecurityPolicyは次のように動いていました。PSPオブジェクトを作成し、ClusterRoleにそのPSPに対する
use権限を入れ、それをPodを作成する主体(人ではなくPodを作成するコントローラーのSA)に
バインドします。するとアドミッション時に、「この主体が使えるPSPのうち、このPodを許可するものがあるか」を探します。
問題がいくつもありました。
- 複数のPSPが一致すると、どれが適用されたのかがわかりにくい状態でした。並べ替えのルールはありましたが、直感的ではありませんでした。
- PSPはmutatingでもあったため、Podスペックを黙って書き換えました。自分が書いたものと、実行されるものが違っていました。
- リクエストの主体が人ではなくコントローラーのSAだという点が、混乱を生み続けました。
- そのためデバッグが地獄でした。「なぜこのPodが拒否されたのか」に答えるのに長い時間がかかりました。
結果として、多くのクラスターがPSPをそもそも有効にしないか、全員にpermissiveなPSPを与えて終わりにしました。 使われないセキュリティ機能は、セキュリティではありません。PSPはv1.25で完全に削除されました。
どう動くのか
3つのプロファイル
| プロファイル | 性格 | 代表的な制限 |
|---|---|---|
| privileged | 無制限 | なし。CNI・ストレージドライバーなどのシステムワークロード向け |
| baseline | 既知の権限昇格だけを遮断 | hostNetwork/hostPID/hostIPCの禁止、privilegedの禁止、hostPathの禁止、危険なcapabilityの追加の禁止 |
| restricted | ベストプラクティスを強制 | baseline + runAsNonRootが必須、seccompProfileが必須、capabilitiesをすべてdrop、allowPrivilegeEscalation=false |
restrictedを満たすには、Podスペックに最低でも4つが必要です。
runAsNonRoot: true、allowPrivilegeEscalation: false、capabilities.drop: [ALL]、
seccompProfile.type: RuntimeDefaultです。
3つのモード
| モード | 動作 | 使うとき |
|---|---|---|
enforce |
違反するPodの作成を拒否 | 実際の適用 |
audit |
監査ログにだけ記録し、作成は許可 | 影響の把握 |
warn |
kubectlユーザーに警告を表示し、作成は許可 | 移行の準備 |
ラベルはpod-security.kubernetes.io/<모드>: <프로파일>の形で(プレースホルダーはモードとプロファイルです)、
<모드>-versionでバージョンを固定できます(プレースホルダーはモードです)。
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
バージョンの固定がなぜ重要なのか。固定しないとlatestになり、クラスターをアップグレードした瞬間に
ポリシーの内容が変わる可能性があります。デプロイした覚えがないのにPodが拒否され始める事故は、ここから起きます。
上の例は、実務で最もよくある組み合わせです。enforceはbaselineで実際に遮断しつつ、 audit/warnはrestrictedで設定して、「restrictedに上げると何が壊れるか」をあらかじめ収集します。
必ず知っておくべき性質: アドミッション時にしか動作しない
PSAが判断するのは、Podが作成されるときだけです。すでに動いているPodは、ラベルを上げても追い出されません。
そのためenforceをrestrictedに上げた直後は何も起きていないように見えますが、
次のロールアウトやノードの入れ替えのときにPodが起動せず、問題が表面化します。ポリシーの変更と事故の間に、時間差があります。
適用除外(exemptions)
ネームスペースのラベルでは表現できない例外があるため、AdmissionConfigurationに3つの軸の適用除外を置きます。
usernames: 特定のユーザーのリクエストは検査しないruntimeClasses: 特定のRuntimeClassを使うPodは検査しないnamespaces: 特定のネームスペースは検査しない(通常はkube-system)
適用除外はクラスター全体の設定なのでapiserverを再起動する必要があり、ネームスペースのラベルでは変更できません。 これは安全装置でもあります。ネームスペースの管理者が自分自身を適用除外にできないからです。
PSAでは足りないとき
PSAはプロファイルが3つしかないため、「このレジストリのイメージだけを許可」「すべてのPodにリソース制限を必須にする」 「チームラベルのないネームスペースを禁止」のようなことは表現できません。そのときはポリシーエンジンを重ねます。
- Kyverno: 学習コストが低く、YAMLでポリシーを書け、リソースの生成(generate)までできます。CNCF Incubatingです。
- OPA Gatekeeper: Rego言語で表現力がとても高く、クロスリソース検証ができます。リソース消費が高く、CNCF Graduatedです。
著者のブログが下した判断は明確です。「単純なポリシーにはKyverno、複雑なクロスリソース検証には OPA Gatekeeper」、そして推奨される組み合わせはRBAC + PSA + ポリシーエンジン(RBACは基本の認可、PSAはPod Securityの基準、ポリシーエンジンはカスタム)です。
Gatekeeperには、アドミッションWebhookが見逃す穴が1つあるため、Audit Controllerが別にあります。
Webhookが止めるのは新規のリクエストだけなので、ポリシーを後から追加すると、すでにデプロイされた違反リソースはそのまま残ります。
Auditが定期的に全体を走査して、それを見つけ出します。enforcementAction: dryrunで遮断せずに
違反だけを収集するのが、新しいポリシーの影響評価の標準的な手順です。
現場での姿
著者のホームラボ再構築の記録に、PSAとまったく同じ原理の出来事があります。
Cilium設置の直後にhubble-relayとhubble-uiがPendingで、理由は
0/1 nodes are available: 1 node(s) had untolerated taint(s)でした。
コントロールプレーンのNoScheduleテイントを、Deploymentがトレラレーションで許容していなかったのですが、
「正常な動作なのに事故のように見える」タイプだという点で、PSAの移行と性格が同じです。
著者の結論も同じでした。「これはエラーではなく、正常な動作です。」
PSA移行の標準的な手順も、著者がまとめたとおりです。
① dry-runで現在の状態を監査(kubectl label --dry-run=server --overwrite ns --all ...)、
② warn/auditを先に適用、③ 違反しているワークロードを修正、④ enforceを有効化、
⑤ AdmissionConfigurationで新規ネームスペースにデフォルト値を適用。
「段階的なアプローチが核心であり、決して一度にenforceを適用しません。」
続けて読むこと
まずシークレット管理の読み物をもう1本読み、そのあとのラボで、ネームスペースにPSAラベルを自分で付けます。 restrictedに違反するPodが拒否されることとその拒否メッセージを確認し、restrictedを通過するPodを 作ってみて、最後にラベルだけを変えて等級を上げたとき、既存のPodはそのまま生き残り、 新しいPodだけが拒否されることを自分で観察します。