Certified Kubernetes Security Specialist (CKS) #11: 分離: gVisor、Kata Containers、RuntimeClass

CKS シリーズ はドメイン Minimize Microservice Vulnerabilities を扱っている最中です。先の #9 Pod Security Admission#10 Secrets 管理 が Pod の権限と秘密データを扱ったとすれば、この記事はもう一歩踏み込んで、コンテナ自体の分離がなぜ弱いのか と、それを補うサンドボックスランタイムを整理します。

コンテナは軽いです。ところがその軽さの代償が、そのままセキュリティの弱点です。コンテナはホストのカーネルをそのまま共有するため、コンテナの中でカーネルの脆弱性を突く攻撃が成功すると、その被害がホスト全体に広がりかねません。この弱点を埋めるのが gVisor や Kata Containers のようなサンドボックスランタイムであり、Kubernetes でそれを選ぶ仕組みが RuntimeClass です。

なぜコンテナ分離は弱いのか #

仮想マシンとコンテナの違いを一行で整理すると カーネルを共有するかどうか です。仮想マシンはゲストごとに自分のカーネルを別々に回し、その下でハイパーバイザがハードウェアを仮想化します。一方コンテナはホストのカーネルをそのまま借りて使いながら、namespace と cgroup で見える範囲だけを分けます。

この構造がコンテナを軽く速くしますが、セキュリティ境界は薄くなります。コンテナの中で回るすべてのシステムコールは、結局 ホストの同じカーネル に渡されます。そのため次のような状況が危険です。

  • コンテナ内のプロセスがカーネルの脆弱性を突くと、その脆弱性はホストカーネルの脆弱性です。コンテナエスケープ (container escape) でホストを掌握できます。
  • 信頼できないコード (外部から受け取ったイメージ、マルチテナント環境のユーザーワークロード) を同じノードで回すと、1 つのコンテナの侵害が隣のコンテナとノードに広がりかねません。

AppArmor と seccomp でシステムコールを制限するのはこの攻撃表面を減らす良い方法ですが、結局同じカーネルを共有するという前提は変わりません。共有カーネル自体をより厚く包むか、いっそ分離しよう というのがサンドボックスランタイムの発想です。

実際に過去に報告された複数のコンテナエスケープ事例は、カーネルやコンテナランタイムの欠陥を利用しました。権限のあるコンテナの設定ミス、カーネルのメモリ処理バグ、ランタイムバイナリを上書きする攻撃などがそこに含まれます。こうした攻撃の共通点は、コンテナとホストが同じカーネルを見ているという事実 をてことして利用する点にあります。したがって信頼できないワークロードを回すときは、カーネル境界自体にもう一枚防御を重ねることに意味があります。

サンドボックスランタイムの 2 つ #

Kubernetes でよく使うサンドボックスランタイムは 2 つの系統です。アプローチが互いに異なるので、原理を区別しておきます。

gVisor (runsc) #

gVisor は Google が作ったサンドボックスランタイムです。核心は ホストカーネルとコンテナの間に、ユーザー空間で回るもう 1 つのカーネルを挟み込む ことです。このユーザー空間カーネルが runsc であり、コンテナが呼ぶシステムコールをホストカーネルへそのまま渡さず、横取りして自分が代わりに処理します。

コンテナがシステムコールを呼ぶと、その呼び出しはまず runsc へ行きます。runsc は Linux システムコールの相当数をユーザー空間で自前で実装しているため、多くの場合ホストカーネルに触れずに応答します。ホストカーネルに実際に届かなければならない呼び出しは、ごく限られた狭い通路でのみ渡されます。結果としてコンテナがホストカーネルと直接接する面積が大きく減ります。

代償は性能です。システムコールごとに一枚を余分に通るので、入出力が頻繁なワークロードやシステムコールが多いワークロードでは性能が落ちます。また runsc が実装していない一部のシステムコールや機能を使うアプリケーションは、互換性の問題が出ることがあります。

Kata Containers #

Kata Containers は別の方向を選びます。コンテナを 軽量仮想マシンの中で 回します。各 Pod (またはコンテナ) が自分だけの軽い VM とその中のゲストカーネルを持つので、分離境界がコンテナレベルではなく VM レベルになります。

こうすると、コンテナの中でカーネルを掌握しても、それはホストカーネルではなくゲストカーネルなので、ホストへ広がるにはハイパーバイザ境界をもう一度越えなければなりません。仮想マシンに準じる強い分離を得るわけです。代わりに VM を立てる分だけ起動時間とメモリ使用が増え、ノードに仮想化サポート (ネスト仮想化など) が必要です。

2 つの方式の比較 #

項目gVisor (runsc)Kata Containers
分離方式ユーザー空間カーネルでシステムコール横取り軽量 VM + ゲストカーネル
分離の強さ強い (攻撃表面の縮小)より強い (VM 境界)
性能負担システムコール・入出力で低下VM 起動・メモリ負担
ノード要件比較的軽い仮想化サポート必要
適した場所信頼の低い一般ワークロード強いマルチテナンシー分離

RuntimeClass #

サンドボックスランタイムをノードにインストールしたからといって、すべての Pod が自動的にそのランタイムを使うわけではありません。Kubernetes には どの Pod をどのランタイムで回すか を選ぶ仕組みが必要で、それが RuntimeClass です。

RuntimeClass はコンテナランタイム設定を指すクラスター範囲のリソースです。核心フィールドは handler であり、この値はノードのコンテナランタイム (containerd など) の設定に定義されたハンドラ名と一致しなければなりません。たとえば containerd に runsc ハンドラを登録しておいたなら、RuntimeClass の handlerrunsc に指定します。

ここで前提が 1 つあります。ノードに該当ランタイムがすでにインストールされていて、コンテナランタイム設定にハンドラが登録されている必要があります。 RuntimeClass はそのハンドラを指す名札にすぎず、ランタイム自体をインストールはしません。試験では普通ハンドラがすでにノードに準備された状態で与えられ、受験者は RuntimeClass を作って Pod につなぐ部分を担います。

YAML で作ってみる #

まず gVisor を指す RuntimeClass を作ります。

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: gvisor
handler: runsc

metadata.name は Pod から参照する名前で、handler はノードランタイムに登録されたハンドラ名です。この 2 つは混同しやすいです。Pod は name (ここでは gvisor) を参照し、ノードランタイムは handler (ここでは runsc) を探します。

作った RuntimeClass を Pod に適用するときは、Pod spec の runtimeClassName に RuntimeClass の名前を書きます。

apiVersion: v1
kind: Pod
metadata:
  name: sandboxed-nginx
spec:
  runtimeClassName: gvisor
  containers:
    - name: nginx
      image: nginx:1.27

runtimeClassName: gvisor を指定した瞬間、この Pod のコンテナはノードで runsc ハンドラを通して、つまり gVisor サンドボックスの中で起動します。ほかの Pod が runtimeClassName を空けておけばノードのデフォルトランタイムで回るので、サンドボックスが必要なワークロードにだけ選択的に 適用できます。

Kata Containers を使う場合も形は同じです。ノードに Kata ハンドラ (たとえば kata) が登録されているなら、RuntimeClass の handler をその名前に変えるだけで済みます。

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: kata
handler: kata

この RuntimeClass を作った後、Pod で runtimeClassName: kata を指定すれば、該当 Pod は軽量 VM の中で回ります。RuntimeClass の形は同一で、指すハンドラだけが変わる構造です。

適用の確認 #

Pod が本当にサンドボックスランタイムの中で回っているかは、Pod の中でカーネル情報を見ると現れます。一般のコンテナはホストカーネルを共有するのでホストと同じカーネル情報を見ますが、gVisor の中では runsc が報告する別の情報が出ます。

# ノードのホストカーネル情報
uname -r

# Pod の中でカーネル情報を確認
kubectl exec sandboxed-nginx -- uname -r

gVisor の中で回る Pod なら、uname -r がホストと異なる、gVisor が模倣するカーネルバージョンを見せます。また dmesg でカーネルログを見ると、一般のコンテナと異なる出力が出ます。gVisor はホストの実際のカーネルリングバッファをそのまま見せず自前のメッセージを出すので、2 つの環境の dmesg 出力が異なります。

# gVisor Pod の中でカーネルログを確認
kubectl exec sandboxed-nginx -- dmesg

uname -rdmesg の出力がホストと異なって出るなら、その Pod がサンドボックスランタイムの中で回っているという合図です。RuntimeClass がきちんと適用されたかを素早く検証する方法として、身に付けておくとよいです。

トレードオフ #

サンドボックスランタイムはタダのセキュリティではありません。分離を強く取る分だけ性能と互換性を差し出す取引です。

  • セキュリティ対性能。 gVisor はシステムコールごとに一枚を通るので入出力が頻繁なワークロードで遅くなり、Kata は VM 起動とメモリのコストがかかります。分離が強いほど一般的に負担も大きいです。
  • セキュリティ対互換性。 gVisor は一部のシステムコールを実装しないので、特定のアプリケーションが動作しないことがあります。ノードデバイスに直接アクセスしたり特殊なカーネル機能を使うワークロードは、サンドボックスと合わないことがあります。
  • 選択的適用。 そのため実務ではすべての Pod をサンドボックスで回しません。信頼水準の低いワークロードやマルチテナント環境の外部コードにだけ RuntimeClass で選択適用し、残りはデフォルトランタイムに置く方式が一般的です。

試験ポイント #

  • RuntimeClass の作成と Pod 指定が定番の作業です。「ノードにすでに登録された runsc ハンドラを使う RuntimeClass を作り、与えられた Pod がそれを使うようにせよ」型が出ます。RuntimeClass を作り、Pod spec に runtimeClassName を入れる 2 ステップを素早く終えられる必要があります。
  • namehandler を区別します。 RuntimeClass の metadata.name は Pod が参照する名前で、handler はノードランタイムに登録されたハンドラ名です。Pod の runtimeClassName には handler ではなく RuntimeClass の name を書かなければなりません。
  • apiVersion を覚えます。 RuntimeClass は node.k8s.io/v1 です。ドキュメントから素早くコピーしてくるほうが安全です。
  • ランタイムはすでにインストールされていると前提します。 試験で受験者が gVisor や Kata をノードにインストールする場合はまれです。ハンドラは準備されていて、RuntimeClass と Pod の接続が採点対象です。
  • 検証コマンドを知ります。 適用の可否が疑わしければ kubectl exec で Pod に入り、uname -rdmesg でホストと異なるか確認します。
  • gVisor ドキュメントの閲覧が許可されます。 gVisor 公式ドキュメント は試験中に閲覧できる指定ドキュメントなので、RuntimeClass の例の場所をあらかじめ覚えておくと時間を節約できます。

まとめ #

この記事で押さえたこと:

  • コンテナ分離が弱い理由はホストカーネルの共有です。 コンテナ内のシステムコールが結局ホストカーネルへ行くので、カーネルの脆弱性はそのままコンテナエスケープの通路になります。
  • gVisor (runsc) はユーザー空間カーネルでシステムコールを横取りして、ホストカーネルと接する面積を減らします。軽いですが性能と互換性のコストがあります。
  • Kata Containers は軽量 VM の中でコンテナを回して、VM レベルの強い分離を得ます。分離はより強いですが、起動・メモリ負担と仮想化要件があります。
  • RuntimeClass は handler でノードのランタイムを指すリソース であり、Pod の runtimeClassName で適用します。ランタイムはノードに事前にインストールされている必要があります。
  • トレードオフはセキュリティ対性能・互換性です。 信頼の低いワークロードにだけ選択的に適用するのが実務パターンです。
  • 検証は uname -rdmesg でホストと異なるか確認します。

次へ: Pod-to-Pod mTLS #

分離で 1 つのノードの中にワークロードを閉じ込めたなら、次はノードの間を行き交う通信を守る番です。同じドメイン Minimize Microservice Vulnerabilities の最後のトピックとして、Pod の間のトラフィックを暗号化して相互認証する mTLS を扱います。

#12 Pod-to-Pod mTLS: Cilium では、Cilium がどのように Pod 間通信に mTLS をかぶせるのか、NetworkPolicy とどう噛み合うのか、そして試験で通信暗号化を要求する作業をどう解くのかを、直接作りながら整理します。

X