Pod を作る権限はあるが、特権 Pod は止めなければならない
目標
ValidatingAdmissionPolicy(VAP)で「特権コンテナの禁止」と「hostPathボリュームの禁止」のポリシーをCELで自分で書き、 バインディングで1つのネームスペースだけに有効にしたうえで、APIサーバーが返す拒否メッセージと、正常なPodの承認の両方を観察します。
なぜ重要なのか
Kubernetesのすべての書き込みリクエストは、認証 → 認可(RBAC) → アドミッションを経なければetcdに保存されません。RBACが答えるのは
「このユーザーがPodを作成してよいか」までで、「そのPodの内容が安全か」は問いません。
Podを作成する権限さえあれば、privileged: trueや、ノードの/をhostPathでマウントしたPodで、ノードをまるごと
乗っ取れます。その穴をふさぐ層が、アドミッション制御です。
Pod Security Admission(PSA)は、ネームスペースのラベル1つで、決まった基準(privileged/baseline/restricted)を有効にします。 便利ですが、組織独自のルール(許可するレジストリ、特定のラベルの必須化など)は表現できません。VAPはAPIサーバーの中で CEL式を直接評価するため、Webhookサーバーがなくても望むルールを書けますし、ポリシー(何を検査するか)と バインディング(どこに、Deny/Warn/Auditのどれで適用するか)が分離されているため、同じポリシーをネームスペースごとに異なる形で有効にできます。
拒否メッセージには、どのポリシーとどのバインディングが止めたのかが名前で出力されます。インシデント対応のとき、「なぜデプロイできないのか」を 最も早く解く手がかりが、まさにこの1行です。また、アドミッションは作成リクエストの時点でしか起きないため、ポリシーを有効にする前に すでに作成されていたオブジェクトはそのまま残るという点も、覚えておいてください。
ステップ
- ネームスペース
kcsa-admを作成し、自動で付くラベルを確認します。 - 特権コンテナを拒否するポリシー
deny-privilegedを、CELで書きます。 - バインディング
deny-privileged-bindingで、kcsa-admだけにDenyで有効にします。 - 特権Podを作成してみて、拒否メッセージを
/root/kcsa-adm/denied-privileged.txtに保存します。 - 平凡なPod
goodが承認されることを確認します。 - hostPathボリュームを拒否するポリシー
deny-hostpathとバインディングを作成します。 - hostPathのPodを作成してみて、拒否メッセージを
/root/kcsa-adm/denied-hostpath.txtに保存します。 - 何が止められ、何が通過したかを
/root/kcsa-adm/report.txtに残します。
参考
- ポリシーだけを作成してバインディングを忘れると、何も止まりません。
kubectl get validatingadmissionpolicybindingで、両方あるかをまず確認してください。 validationActionsをWarnにすると、リクエストは通過して警告だけが表示されます。止めるにはDenyである必要があります。- このラボのPodはコンテナなしで模擬的に動かしますが、アドミッションはAPIリクエストの段階で起きるため、拒否・承認は本物です。
- 公式ドキュメント: Validating Admission Policy・ Pod Security Standards。
ポリシーを適用する分離区画を作る
ネームスペースkcsa-admを作成してください。このネームスペースには、APIサーバーが自動でkubernetes.io/metadata.name=kcsa-admラベルを付け、後のステップのバインディングが、このラベルで対象を選びます。
Kubernetesは、すべてのネームスペースに、自分の名前を値とするkubernetes.io/metadata.nameラベルを自動で付けます。作成したあとで、kubectl get ns <이름> --show-labelsで確認してみてください(プレースホルダーはネームスペース名です)。createを--dry-run=clientで生成してapplyすれば、何度実行しても安全です。
特権コンテナを止めるルールをCELで書く
ValidatingAdmissionPolicydeny-privilegedを作成してください。PodのCREATEリクエストで、securityContext.privilegedがtrueのコンテナが1つでもあれば拒否するCEL式を書き、拒否メッセージはprivileged containers are not allowedにします。failurePolicyはFailです。
ポリシーは、matchConstraints.resourceRulesでどのリクエスト(コアグループv1のpods、CREATE)を見るかを決め、validations[].expressionが真であれば通過します。object.spec.containers.all(c, ...)のように、すべてのコンテナが条件を満たすかを問いますが、securityContextやprivilegedフィールドがそもそもない場合も通過として扱う必要があるので、has()で先に確認してください。ポリシーだけでは、何も止まりません。
ルールをkcsa-admに結び付けて実際に有効にする
ValidatingAdmissionPolicyBindingdeny-privileged-bindingを作成してください。policyNameはdeny-privileged、validationActionsは["Deny"]、対象はmatchResources.namespaceSelectorのmatchLabelsでkubernetes.io/metadata.name: kcsa-admのネームスペースだけを選びます。
VAPは、ポリシー(何を検査するか)とバインディング(どこに、どんな動作で適用するか)に分かれています。バインディングがなければ、ポリシーは評価すらされません。validationActionsにはDeny以外にWarn・Auditもありますが、Denyであって初めてリクエストが実際に拒否されます。ネームスペースを選ぶときは、ステップ1で確認した自動ラベルを使ってください。
特権Podが入口で追い返される
ネームスペースkcsa-admに、securityContext.privileged: trueのコンテナを持つPod(イメージはnginx:1.27-alpine)を作成してみて、APIサーバーが返した拒否メッセージ全体を/root/kcsa-adm/denied-privileged.txtに保存してください。
拒否はPodが作成される前に起きるため、kubectlが0以外の終了コードとともにエラーを標準エラー出力に出力します。標準エラー出力までファイルに受け取る必要があります(2>&1)。メッセージには、どのポリシーとどのバインディングが拒否したのかが名前で入っています。バインディングの直後は、反映に1–2秒かかることがあるので、Podが作成されてしまったら削除して、もう一度試してください。
平凡なPodはつかえずに入る
ネームスペースkcsa-admに、特権の設定がない平凡なPodgood(イメージはnginx:1.27-alpine)を作成してください。ポリシーが有効になっていても、このPodは承認される必要があります。
よいポリシーは、悪いリクエストだけを止めて、正常なリクエストはそのまま通します。CEL式でsecurityContextのないコンテナを通過として処理できているかが、ここで明らかになります。作成されていればkubectl get pod good -n kcsa-admで見え、止められていれば、ステップ4と同じ形のエラーが出ます。
ノードのディスクに通じるhostPathも止める
ValidatingAdmissionPolicydeny-hostpathとバインディングdeny-hostpath-bindingを作成してください。ポリシーは、PodのCREATEでhostPathボリュームが1つでもあれば拒否し(メッセージはhostPath volumes are not allowed、failurePolicy: Fail)、バインディングはvalidationActionsを["Deny"]にして、kubernetes.io/metadata.name: kcsa-admのネームスペースにだけ適用します。
構造はステップ2・3と同じで、検査の対象だけがvolumesに変わります。volumesフィールドがそもそもないPodもあるため、has(object.spec.volumes)で先に確認し、各ボリュームにhostPathフィールドがあるかをhas()で問いかけてください。ポリシーとバインディングを---でつないで、一度にapplyしてもかまいません。
ノードの/etcをマウントしようとしたPodが拒否される
ネームスペースkcsa-admに、ノードの/etcをhostPathボリュームとしてマウントするPod(イメージはnginx:1.27-alpine)を作成してみて、拒否メッセージ全体を/root/kcsa-adm/denied-hostpath.txtに保存してください。
PodのspecのvolumesにhostPathボリュームを置き、コンテナのvolumeMountsでマウントする形です。ステップ4と同じように、標準エラー出力をファイルに受け取ってください。今回のメッセージに出力されるポリシーとバインディングの名前が、ステップ4と違うことを確認してください。
何が止められ何が通過したかを帳簿として残す
観察結果を/root/kcsa-adm/report.txtに4行で書いてください。privileged=denied、hostpath=denied、plain=allowed、enforcement=validatingadmissionpolicyです。採点ツールは、各行を実際のクラスターにPodを作成して照合します。
値はdeniedかallowedのどちらかで、最後の行は、この拒否を誰が実行したか(PSAラベルではなく、どのアドミッションの仕組みか)を小文字の1単語で書きます。推測ではなく、ステップ4・5・7で自分の目で見た結果を書き写してください。