ラベル三行がPSPを置き換えた理由
一言でいうと
Pod Security Admissionは、ネームスペースのラベル数行で、ワークロードのセキュリティの下限を決めます。そしてSecretはbase64でエンコードされているだけで暗号化されていないため、別の統制が必要です。
なぜ必要なのか
PodSecurityPolicyは、1.21でdeprecatedになり、1.25で削除されました。理由は2つありました。PSPを使うには、ワークロードのServiceAccountに「このPSPをuseできる」というRBAC権限を与える必要があり、その構造そのものが権限昇格の経路を作ってしまいました。そして、どのPodにどのPSPが適用されるのかを予測するのが非常に困難でした。複数のPSPの中から1つが選ばれるルールが複雑だったためです。
代替のPSAは正反対にシンプルです。ネームスペースにラベルを付ければ終わりです。
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.30
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
enforceは違反したPodを拒否し、auditは監査ログにだけ記録し、warnはkubectlのユーザーに警告を表示するだけです。3つのモードを別々に設定できることが、移行の核心です。既存のクラスターにいきなりenforce: restrictedを設定するとワークロードが大量にブロックされるため、warnとauditを先に有効にして何が引っかかるかを観察してから、enforceに上げます。
-versionラベルも重要です。プロファイルの定義は、Kubernetesのバージョンごとに少しずつ強化されます。バージョンを固定しておけば、クラスターのアップグレードがそのままポリシーの強化につながり、Podが突然拒否されるという事態を防げます。
どう動くのか
プロファイルは3段階です。
| プロファイル | 主な制限 |
|---|---|
| privileged | 制限なし。CNIやストレージドライバーのようなシステムワークロード向け |
| baseline | hostNetwork/hostPID/hostIPCの禁止、privilegedの禁止、hostPathの禁止、危険なcapabilityの禁止 |
| restricted | baseline + runAsNonRootが必須、seccompProfileが必須、capabilities drop ALLが必須、allowPrivilegeEscalation falseが必須 |
restrictedを通過するPodは、4つのフィールドを必ず持ちます。runAsNonRoot: true、allowPrivilegeEscalation: false、capabilities.drop: ["ALL"]、seccompProfile.type: RuntimeDefaultです。この4つのうち1つでも欠けると拒否され、エラーメッセージにどのフィールドがなぜ引っかかったかがそのまま表示されます。
2つ目の軸はSecretです。実務者が最もよく誤解する部分です。Secretマニフェストの値がbase64になっているため暗号化されているように見えますが、base64はエンコードであって暗号化ではありません。鍵がなく、元に戻すのにコマンド1つで済みます。デフォルトの設定では、Secretはetcdに平文で保存されるため、etcdのバックアップファイルやディスクイメージを手に入れた人は、すべてのSecretを読めます。保存時の暗号化は、EncryptionConfigurationファイルを作成して、kube-apiserverの--encryption-provider-configで指定することで有効になります。このとき、identityプロバイダーは必ずリストの最後でなければなりません。先頭に来ると、平文での保存に戻ります。設定を変えても、既存のSecretは書き直されるまで平文のままなので、全体を一度更新する必要があります。
3つ目は注入の方式です。環境変数として入れると、/proc/<PID>/environで読まれ、子プロセスに継承され、クラッシュレポートやデバッグページにまるごと載って流出します。ボリュームマウントのほうが安全で、defaultMode: 0400で所有者の読み取りだけを許可するのが慣例です。
現場での姿
etcdを直接開いてみると、誤解が解けます。暗号化を有効にしていない状態でetcdctl get /registry/secrets/default/app-db | hexdump -Cを実行すると、キーのパスと値がそのまま見えます。暗号化を有効にすると、同じ場所にk8s:enc:kms:v2:のようなプロバイダーの接頭辞が付き、本文は読めなくなります。この違いを自分の目で見た人と見ていない人とでは、設計が違います。
RBAC側の誤解もよく目にします。あるネームスペースでget secrets権限を持つ人は、そのネームスペースのすべてのSecretを読めます。開発者に便宜上与えた編集権限が、事実上、本番の認証情報の閲覧権限になっていることが非常によくあります。kubectl auth can-i get secrets --namespace payments --as dev@example.comの1行で確認できますが、確認してみたチームはまれです。RoleのresourceNamesで特定のSecretだけを許可する方式があることも、あまり知られていません。
マルチテナンシーの観点でも、同じ話が繰り返されます。ネームスペースベースの隔離は、APIサーバーを共有するため、CRDのインストールの衝突やノイジーネイバーの問題が残ります。vClusterのように、テナントごとに独立したAPIサーバーを与える方式はこの問題を解決しますが、ワークロードのPodは結局、ホストクラスターのネームスペースに<vcluster>-x-<이름>-x-<네임스페이스>(プレースホルダーは名前とネームスペースです)という形で実体化されます。つまり、ホスト側のPSSとNetworkPolicyのハイジーンは、そのまま必要です。抽象化レイヤーが1つ増えても、下位の統制が不要になるわけではありません。
次のラボですること
まず2つのネームスペースにbaselineとrestrictedをそれぞれ設定し、restrictedを通過するPodを作成してから、違反したPodが実際に拒否されることを確認します。次にSecretのラボで、タイプ別の作成、環境変数での注入とボリュームマウントの違い、defaultMode、immutable Secret、resourceNamesで絞り込んだRBAC、そしてEncryptionConfigurationファイルの作成までを扱います。