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

KCSA — Kubernetesセキュリティアソシエイト

スケジューラ、kube-proxy、コンテナランタイム — 静かな構成要素の資格情報

TT Labで続きを見る

一言でいうと

kube-schedulerとkube-proxyは、それぞれAPIサーバーに対するクライアント証明書を持ち、自分のHTTPSポートを開いています。コンテナランタイムは、Unixソケット1つでノードのすべてのコンテナを作成し、削除します。3つのコンポーネントは監査ログに捕捉されにくい場所にあるため、何を握っていて、どこを開いているのかを知り、その入口を絞る必要があります。

なぜ必要なのか

KCSAのクラスターコンポーネントのセキュリティのドメインでは、APIサーバー・etcd・kubeletだけが問われるわけではありません。攻撃者は、最も堅く守られた入口を叩きません。スケジューラーのkubeconfig、kube-proxyのメトリクスポート、ランタイムソケットのように、管理者があまり目を向けない認証情報とポートを探します。APIサーバー迂回リスクのドキュメントは、まさにこの観点で書かれています。APIサーバーを経由せずにクラスターを変更できる経路は、アドミッション制御も監査ログも通りません。

どう動くのか

kube-scheduler: バインディング権限を握るコントロールプレーンのプロセス

kube-schedulerリファレンスのドキュメントによると、スケジューラーは、スケジューリングキューのPodごとに、制約と利用可能なリソースに合うノードを選んで順位を付け、バインドするコントロールプレーンのプロセスです。このバインディング権限が、そのまま脅威モデルになります。スケジューラーの認証情報(kubeadmでは/etc/kubernetes/scheduler.confのクライアント証明書)を手に入れた攻撃者は、Podを好きなノードに載せられ、機微なワークロードと同じノードに自分のPodを置けます。

スケジューラーは、自分のHTTPSポートも開きます。ポートとプロトコルの表では10259です。このポートの認証・認可は、2つのフラグが決めます。--authentication-kubeconfigはTokenReviewを作成できるkubeconfigを指しており、空にするとすべてのトークンリクエストが匿名として扱われます。--authorization-kubeconfigはSubjectAccessReviewを作成できるkubeconfigを指しており、空にすると、認可をスキップしないリクエストはすべて拒否されます。つまり、2つのフラグをAPIサーバーに委任する設定にしておかなければ、メトリクス・ステータスのエンドポイントが誰でも読める入口になってしまいます。--leader-electはデフォルトでtrueなので、複製されたスケジューラーのうち1つだけが動作します。

スケジューラーを迂回する経路も知っておく必要があります。ノードのドレインのドキュメントは、PodのnodeNameを直接指定すると、スケジューラーを経由せずにそのノードに結び付けられると記しています。Podを作成する権限のあるユーザーは、スケジューラーのポリシーと無関係にノードを選べるため、機微なノードの分離は、スケジューラーの設定ではなく、アドミッション(PodNodeSelectorなど)とテイントで強制する必要があります。

kube-proxy: ノードごとに動くネットワークプロキシ

kube-proxyリファレンスのドキュメントは、kube-proxyが各ノードで動き、APIのServiceの定義をノードに反映して、TCP・UDP・SCTPをバックエンドに転送すると説明しています。Linuxのプロキシモードは、iptables(デフォルト)、ipvs、nftablesです。kube-proxyはServiceを実装するものであって、NetworkPolicyを実行するものではありません。ポリシーの実行は、次の読み物で扱うCNIの役割です。

セキュリティの観点で見るべきことは2つです。1つ目は認証情報です。PKI証明書と要件のドキュメントは、kube-proxyがAPIサーバーに認証するためのクライアント証明書が、ノードごとにあると記しています。ノードに置かれたこのkubeconfigを読めるプロセスは、kube-proxyの権限でAPIサーバーと通信できます。2つ目は、開いているポートです。--healthz-bind-addressのデフォルト値は0.0.0.0:10256なので、すべてのインターフェースでステータスを受け付けますが、--metrics-bind-addressのデフォルト値は127.0.0.1:10249で、ローカルでのみメトリクスを出します。モニタリングのためにこれを0.0.0.0:10249に広げると、ノードネットワークの誰もがkube-proxyの内部状態を読めてしまいます。デフォルト値がなぜローカルなのかを理解したうえで広げる必要があります。

コンテナランタイム: ソケット1つがノード全体である

CRIのドキュメントによると、kubeletはgRPCクライアントとしてランタイムに接続し、エンドポイントはUnixソケットです(containerdは/var/run/containerd/containerd.sock、CRI-Oは/var/run/crio/crio.sock)。APIサーバー迂回リスクのドキュメントの「コンテナランタイムソケット」のセクションが、脅威を記しています。このソケットにアクセスする攻撃者は、新しいコンテナを起動したり、動いているコンテナと対話したりでき、そのノードのコンテナがSecretを持っているなら、ほかのノードやコントロールプレーンへ権限を広げられます。

ドキュメントが示す緩和策は、ファイルシステムのレベルです。ソケットに対するファイルアクセスをrootに制限し、kubeletをカーネルのネームスペースでほかのコンポーネントから分離し、ランタイムソケットを含むhostPathのマウントを、直接であれ上位のディレクトリ経由であれ禁止し、hostPathは読み取り専用にして、ディレクトリの制限を迂回できないようにし、ノードへのユーザーアクセスそのものを制限します。同じドキュメントの「static Pod」のセクションも、ランタイムとつながります。kubeletがマニフェストディレクトリから直接起動するstatic PodはAPIサーバーが管理しないため、そのディレクトリに書き込める攻撃者は、アドミッションを経由せずに、hostPathを使うPodをノードに載せられます。static Podがアドミッションに失敗すると、kubeletはAPIサーバーに登録しませんが、Podはノードでそのまま動きます。

ランタイムの分離は、反対方向の道具です。RuntimeClassのドキュメントは、Podごとに異なるランタイム構成を選んで、性能とセキュリティのバランスを取れると説明しています。高水準の情報セキュリティの保証が必要なワークロードは、ハードウェア仮想化を使うランタイムで動かして追加の分離を得る代わりに、オーバーヘッドを受け入れるというやり方です。ノードのCRI実装にハンドラーを設定してRuntimeClassオブジェクトを作成すれば、PodスペックのruntimeClassNameで選びます。

現場での姿

メトリクスの収集のために、kube-proxyを0.0.0.0で開けました。Prometheusがノードの外から10249を取得する必要があるとして、metricsBindAddressを広げた結果、ノードネットワークのどのPodでも、hostNetworkなしでノードIPの10249に届くようになったケースです。広げるなら、NetworkPolicyやファイアウォールで、収集ツールだけを許可するルールも一緒に必要です。

CIランナーがランタイムソケットをマウントしています。イメージをビルドするとして、/var/run/containerd/containerd.sockをhostPathで入れたPodは、ノードのすべてのコンテナを操作できるPodです。ドキュメントがまさにこのマウントを禁止せよと述べる理由であり、Pod Security StandardsのbaselineがhostPathを防ぐ理由でもあります。

次の理論で見ること

次の読み物では、境界にある3つのコンポーネントを見ます。NetworkPolicyを実際に実行するCNI、hostPathとCSIの認証情報が絡み合うストレージ、そしてkubeconfigとexecプラグインが露出するクライアント側のリスクです。