Blog

2023.03.30

Engineering

Kubernetes クラスタの PodSecurityPolicy を Gatekeeper に移行しました

Hidehito Yabuuchi

はじめに

PFN では、計算クラスタ MN-2MN-3 の運用に Kubernetes を採用しており、3, 4 ヶ月に一度のペースで Kubernetes をアップグレードしています。直近のアップグレードは 2023 年 1 月で、Kubernetes 1.24 から 1.25 に更新しました。

Kubernetes 1.25 ではいくつかの API が削除されましたが、とりわけ影響が大きかったのは PodSecurityPolicy (PSP) の削除でした。PSP は、pod が満たすべきセキュリティポリシを定義し、Pod リソースの定義をそれに適合するよう変更したり、適合しない pod の作成を拒否したりする機能です。しかし、PSP はユーザビリティに大きな問題を抱えており、互換性を保ったままそれを解決するのも難しいという理由から、Kubernetes 1.21 から非推奨になっており、1.25 でついに削除されました。PFN でのクラスタの運用でも、PSP の挙動のわかりにくさをたびたび感じていました。PSP 廃止の背景について詳しくは、Kubernetes 公式ブログ記事 PodSecurityPolicy Deprecation: Past, Present, and Future | Kubernetes をご覧ください。

PFN ではそれまで PSP を使い、HostPath ボリュームのマウントや UID と GID の利用、カーネルパラメータの設定、Linux capabilities の有効化などを制限するセキュリティポリシを適用していました。HostPath と UID / GID の制限は、安全に NFS やノードローカルストレージへの読み書きを許可するために導入していました。例えば、社内のクラスタユーザごとに UID / GID を特定の値のみ許可することで、別のユーザのデータへのアクセスを禁止することなどを目的としています。

Kubernetes 1.25 へのアップグレードにともない PSP が利用できなくなることを契機に、PFN では現状のセキュリティレベルを落とさないまま、より扱いやすいセキュリティポリシを施行するシステムを選定・設計し、移行しました。本記事では、どのように移行先を選定したか、どのようにポリシを設計したか、また、どのように移行を完遂したかを紹介します。

移行先の選定

この移行では、現状のセキュリティレベルを落とさないことを第一に重視しました。そのため、PSP の代替として利用できるかという観点で移行先の選定をおこないました。PFN でのユースケースでは、PSP が備えていた以下の機能がとくに必要でした。

  1. PFN のクラスタ用にカスタマイズしたセキュリティポリシを利用できること
  2. Pod リソースの定義によって適用するポリシを切り替えられること

機能 1 については、例えば特定の HostPath ボリュームのマウントを許可したり、複数の NIC を割り当てた pod で特定のカーネルパラメータを設定することを許可したりするために必要です。機能 2 については、例えば HostPath のマウントを、社内ユーザの pod が root 権限で動く場合は禁止し、一般ユーザ権限で動く場合は許可するために必要です。

この要件を踏まえ、PSP からの移行先として以下の 3 つを検討しました。

このうち、PSA は PSP の反省を踏まえシンプルな機能にまとまっています。しかし、セキュリティポリシの定義が Pod Security Standards に固定されカスタマイズできない点で採用できませんでした。また、pod にどのポリシを適用するかがネームスペースごとに決まってしまい、Pod リソースの定義によって切り替えることができない点も、PFN での要件に合いませんでした。

サードパーティプロジェクトでは Gatekeeper と Kyverno を検討しましたが、PFN のユースケースでは両者に大きな違いはありませんでした。ともにセキュリティポリシを自分で定義でき、また、別途 Kubernetes の Admission Webhook 機能と組み合わせれば Pod リソースの定義によって適用するポリシを選択できます。強いて言えば、Gatekeeper はポリシを Rego 言語で記述し、Kyverno は YAML 上に独自の記法を導入してポリシを表現していますが、ポリシが複雑になってくると素直に Rego を書いたほうがわかりやすそうだという感覚をもちました。

結局、まず Gatekeeper で Proof of Concept としてセキュリティポリシを作成してみて、違和感なく作成できたのでそのまま Gatekeeper を採用しました。なお、ポリシの内容は Gatekeeper Library にあるもので事足りたので、Rego はほぼ書かずに済みました。ただし、PSP の allowedUnsafeSysctls に相当する機能だけまだ Gatekeeper Library で実現できなかったため、pull request を作成しました

制約リソースの設計

次に、作成した Proof of Concept をもとに、セキュリティポリシを Gatekeeper で記述していきます。

Gatekeeper ではポリシの定義を Kubernetes リソースで表現します。まず、パラメタライズされた制約のテンプレートを ConstraintTemplate リソースで定義すると Gatekeeper のコントローラが CustomResourceDefinition (CRD) を作り、次に、パラメータを指定してこの CRD からカスタムリソースを作成することで制約が定義できます。Gatekeeper では、有効にする Linux capabilities の制限やホストネットワークの使用可否など、pod の属性ごとに制約をそれぞれ別のリソースとして定義します。

例えば、以下のリソースは Gatekeeper Library にある k8spspforbiddensysctls ConstraintTemplate から作られた CRD のカスタムリソースで、設定できるカーネルパラメータの種類を allowedSysctls パラメータに指定したもののみに制限する制約を定義しています。また、spec.match フィールドはどの Kubernetes リソースにこの制約を適用するかを設定しており、リソースの種類やラベルなどによる条件を記述できます。

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPForbiddenSysctls
metadata:
  name: baseline-forbidden-sysctls
spec:
  match:
    scope: Namespaced
    kinds:
    - apiGroups:
      - ""
      kinds:
      - Pod
    labelSelector:
      matchLabels:
        gatekeeper.k8s.preferred.jp/policy: baseline
      matchExpressions:
      - key: gatekeeper.k8s.preferred.jp/skip-forbidden-sysctls
        operator: DoesNotExist
  parameters:
    allowedSysctls:
    - kernel.shm_rmid_forced
    - net.ipv4.ip_local_port_range
    - net.ipv4.ip_unprivileged_port_start
    - net.ipv4.tcp_syncookies
    - net.ipv4.ping_group_range

このように、Gatekeeper では制限したい pod の属性ごとに制約が別々の Kubernetes リソースとなり、またどの pod に適用するかも制約ごとに異なるものが定義できます。そのため、どのような制約リソースを定義し、それらをどう組織化するかを明確に設計することが重要になってきます。そうすることで、セキュリティポリシを対象となる pod に漏れなく適用でき、また pod にどの制約が適用されるかを簡単に把握できるようになります。

制約リソースの設計では、PSP が備えていた利点を引き継ぎ、PSP が抱えていた欠点を解決することを念頭に設計しました。PFN での運用では、PSP について以下の利点と欠点を感じていました。

  • 利点
    1. Pod ごとに一つの PSP が適用され、PodSecurityPolicy リソース一つにセキュリティポリシの内容がまとまっている
    2. Pod にどの PSP が適用されたかを簡単に把握できる(pod の kubernetes.io/psp アノテーションに記録されるため)
    3. ポリシに適合するよう、runAsUser フィールドなど Pod リソースの定義を自動で mutate する
  • 欠点
    1. 複数の PodSecurityPolicy リソースが利用可能な場合、pod にどれが適用されるかわかりにくい
    2. Pod リソースの定義の mutation が暗黙的におこなわれる

利点 1 について、Gatekeeper では pod の属性ごとに制約が別々のリソースになってしまい、このままではポリシ全体を把握することが難しいです。利点 2 について、Gatekeeper では制約リソースがその spec.match フィールドの条件にマッチした pod に適用されるという、制約側から適用対象の pod を選ぶ形であるため、このままでは pod にどの制約リソースが適用されたかわかりにくいです。

これらの問題を解決するため、セキュリティポリシの単位で Gatekeeper の制約リソースを集めて論理的なグループにまとめ、「ポリシ」を構成するという設計方式を取りました。ポリシは、ポリシ名を表すラベルと一対一で対応します。例えば、baseline ポリシには gatekeeper.k8s.preferred.jp/policy: baseline ラベルが対応します。また、あるポリシに含まれる制約リソースはすべて、そのポリシ名のラベルをもつ pod に適用されるような spec.match フィールドをもちます。逆に言うと、例えば pod に gatekeeper.k8s.preferred.jp/policy: baseline ラベルを付与すると、baseline ポリシを構成するすべての制約リソースがこの pod に適用されます。Pod にどのポリシが適用されるかが pod のラベルに明示され、pod 側から適用されるポリシを選ぶ形になったことがわかります。このように制約リソースをポリシとしてまとめることで、PSP の利点 1, 2 を引き継ぎつつ、欠点 1 を解決することができました。

Gather Gatekeeper constraint resources into a policy

図1:Gatekeeper の制約リソースを集めてポリシを構成する

また、Gatekeeper では Kubernetes リソースの制約だけでなく、mutation にも対応しています。例えば、Pod リソースの runAsUser フィールドに社内ユーザごとの UID を自動で書き込むといったことが可能です。この mutation は Gatekeeper が提供する Assign などのカスタムリソースで明示的に表現するため、PSP の利点 3 を引き継ぎつつ、欠点 2 を解決することができました。

PSP の代替として、PFN では以下の 5 つのポリシを用意しました。

  • 社内ユーザの pod 向け:スムーズな移行を重視した、それまでの PSP と同等のポリシ
    • root(pod が root 権限で動くことを許可するポリシ)
    • non-root(pod が一般ユーザ権限で動くことを強制するポリシ)
  • クラスタアドオン向け:Pod Security Standards に準拠したポリシ
    • restricted
    • baseline
    • privileged(制約リソースをもたない、仮想的なポリシ)

社内ユーザの pod 向けについて、root ポリシは non-root ポリシに比べ UID / GID 以外の制約を強めセキュリティを担保したポリシとなっています。別途 mutating admission webhook で Pod リソースの定義を参照し、すでにユーザ自身でポリシが選択されていない場合は、rootnon-root のどちらのポリシを適用するか選択し gatekeeper.k8s.preferred.jp/policy ラベルを付与しています。また、root または non-root のいずれかのラベルが付与されていることを、Gatekeeper の別の制約リソースで検証しています。

Mutating admission webhook and Gatekeeper in cooperation apply a proper policy to company users' pods

図2: Mutating admission webhook と Gatekeeper が連携して社内ユーザの pod に適切なポリシを適用する

クラスタアドオン向けのポリシについては、Pod Security Standards から外れたカスタムのポリシを使いたい場合に対応するため、pod のラベルで指定することで制約リソースごとに無効化できるようにしています。また、クラスタアドオンの pod が gatekeeper.k8s.preferred.jp/policy ラベルをもたずポリシを選択していない場合は、デフォルトで restricted ポリシが適用されるようにしています。

さらに、ネームスペースごとにデフォルトのポリシを設定できるしくみも構築しました。例えば、ネームスペースに gatekeeper.k8s.preferred.jp/default-policy: privileged ラベルを付与しておくと、このネームスペースに作成される pod にデフォルトで gatekeeper.k8s.preferred.jp/policy: privileged ラベルが付与され privileged ポリシが適用されることになります。このしくみは Gatekeeper の AssignMetadata リソースを使い実現しています。このしくみを活用し、kube-system ネームスペースに作られる、自動でインストールされる CoreDNS などのクラスタアドオンに privileged ポリシが適用されるようにしています。

移行

最後に、クラスタ上の pod に適用されるセキュリティポリシを、PSP から Gatekeeper のポリシに移行していきます。

PFN では、社内ユーザが使う本番用 Kubernetes クラスタの他に、クラスタ管理者がクラスタの開発などに使う評価用 Kubernetes クラスタがあります。まず評価用クラスタから以下の手順で移行をおこない、同じ手順を本番用クラスタについても繰り返しました。

  1. Gatekeeper の制約リソースをすべて warn モードで作成する
  2. ポリシ違反が解消されるまで、クラスタアドオンの pod テンプレートをコンポーネントごとに修正する
  3. 制約リソースをすべて deny モードに変更する
  4. PSP の admission plugin を無効化する
  5. クラスタ上の PodSecurityPolicy リソースを削除する
  6. Kubernetes を 1.25 にアップグレードする

手順 1 について、Gatekeeper では制約リソースの spec.enforcementAction フィールドで、制約に違反したときの挙動を設定できます。warn に設定すると、検証対象のリソースが制約に違反した場合でもリソースの作成や更新をブロックせず、kubectl などのクライアントに警告メッセージを返すのみになります。

この時点ではすべてのクラスタアドオンに restricted ポリシがデフォルトで適用されており、restricted ポリシはそれまでの PSP より厳格なポリシとなっています。制約リソースを warn モードで作成することで、クラスタアドオンを動かしたまま Gatekeeper のポリシに移行することが可能になります。

手順 2 について、ポリシ違反の記録は Gatekeeper の監査ログ から取得しました。ポリシ違反は制約リソースのステータスにも記録されますが、記録される数に上限があるため監査ログを見るほうが確実です。

クラスタアドオンはアプリケーションの内容を変えずに restricted ポリシを適用できるものがほとんどだったため、securityContext フィールドを明示的に設定することで restricted ポリシに適合するよう pod テンプレートを修正しました。CNI プラグインや MN-Core を制御するためのコンポーネントなど、restricted ポリシが適用できないクラスタアドオンについては、baseline ポリシや privileged ポリシを指定し、必要に応じて一部の制約リソースを無効化しました。

手順 3 について、Gatekeeper の制約リソースの spec.enforcementAction フィールドを deny にすると、制約に違反するリソースの作成や更新を拒否するようになります。deny に設定した後で、クラスタアドオンを一通りローリングリスタートし、制約違反なく正常に起動することを確かめました。

手順 4, 5 を Kubernetes アップグレードの前におこなったのは、etcd に PodSecurityPolicy リソースのデータが残ってしまうのを防ぐためです。実害はありませんが、Kubernetes を 1.25 に更新してしまうと PSP の API が削除されるため、PodSecurityPolicy リソースの削除すらできなくなり etcd にデータだけ残り続けてしまいます。これを防ぐため、まず Kubernetes のコントロールプレーンで PSP の admission plugin を無効化することで PSP 機能がはたらかないようにし、その後クラスタから PodSecurityPolicy リソースをすべて削除しました。

最後に Kubernetes を 1.25 にアップグレードし、 PSP から Gatekeeper への移行が完了しました。なお、社内ユーザの pod に適用される Gatekeeper ポリシはそれまでの PSP と互換のポリシとしたため、ユーザによる移行作業は完全に不要でした。

まとめと今後

PFN では、Kubernetes 1.25 へのアップグレードを契機にセキュリティポリシを施行するシステムを PSP から Gatekeeper に移行しました。PSP の利点を引き継ぎつつ欠点を解決するよう Gatekeeper の制約リソースを組織化し「ポリシ」を構成しました。社内ユーザに影響なくすべてのクラスタアドオンを Gatekeeper のポリシに適合させていき、スムーズな Kubernetes 1.25 への更新を達成しました。

Gatekeeper はセキュリティポリシの適用だけでなく、より一般的なポリシを Kubernetes リソースに適用することにも利用できます。PFN には社内クラスタの仕様に合わせさまざまなポリシを適用している admission webhook があるので、今後その機能を Gatekeeper に移植していくことを検討しています。複雑なロジックが不要なポリシを Gatekeeper のリソースで表現することで、ポリシを宣言的に記述できることや、リリース作業を簡略化できることといったメリットを得られると考えています。

  • Twitter
  • Facebook