TT Lab
ブログ

한국어 English 日本語 中文

はじめる
ブログ

ブログ

学生VMが起動するまで 6:隔離 — ゲスト内のrootがクラスターに手を出せない理由

ステータス:ドラフト (2026-10-16)、私たちのマニフェスト・コード + 一部実測。 ポリシーと権限はリポジトリのマニフェストに従っており、執筆時点にクラスターに適用されているポリシー・Roleを読み取り専用で取得してリポジトリと一致しているか比較しました(異なる点は本文に記載しています)。内部アドレスとサーバー名は公開記事のため一般化しています。執筆ルール:実測データが基本であり、測れなかったものは「未実測」、ドキュメントから見つからなかったものは「未確認」と表示します。

学生VMが起動するまで 6:隔離

目次

VMは実習用Podより 広い権限を持つ場所です。学生はゲスト内のrootであり、カーネルモジュールをロードし、任意のパッケージをインストールし、ネットワークツールを自由に使います。実習用Podのようにcapabilityを0に制限する方法は、VM内では通用しません。そのため、VMの隔離は「ゲスト内で何をさせないか」ではなく 「ゲストの外へ何が届くか」 を防ぐ方向に設計しました。

학생 VM 이 닿는 곳과 못 닿는 곳 들어오는 쪽 — 기본 차단, 예외 하나 ✓ 백엔드 웹 파드 → 8899 채점 에이전트 · VM별 토큰 ✗ 실습 파드 → 8899 시간 초과로 막힘(실측) ✗ 다른 학생 VM → 아무 포트 같은 네임스페이스여도 학생 VM (게스트 안 root) 서비스 계정 토큰 없음 · 디스크는 쓰기 층 하나만 virtio 로 보인다 나가는 쪽 — 인터넷만 ✓ DNS · 공인 인터넷 80/443 apt · 설치 스크립트 ✗ 사설 대역 전체 저장소 · 레지스트리 · 공유기 ✗ 쿠버네티스 API 시간 초과(실측) 백엔드 서비스 계정의 권한 (저장소의 Role) ✓ VM·VMI 만들기·지우기, console 서브리소스 ✓ Secret·서비스 만들기·지우기(세션 것) ✓ PVC get — 골든 이름 12개만(resourceNames) ✗ 파드 create, PVC create·delete, DataVolume 이 네임스페이스는 특권 파드를 허용한다 — 파드 create 는 곧 노드 root
図:学生VMが届く場所と届かない場所

1. 入ってくる方向:デフォルト拒否、例外は1つ

ゲスト内の採点エージェントは受け取った文字列を シェルで実行します(第1回 ③)。エージェントはすべてのインターフェース(8899ポート)でリッスンします。そのため、このポートに到達できる側は、そのVM内でrootとして任意のコマンドを実行できます。真っ先に防がなければならない経路です。

最初は実習用PodがVMに到達できないのを見て安心していました。しかし、それは実習用Podの 出る方向 のポリシーのおかげであって、VM側の防御ではありませんでした。Podのポリシーが1行変わるだけで消え去る保護です。「 防御は守りたいものの傍らに置く」という原則に基づき、VMネームスペースに入ってくる方向のポリシーを別途適用しました(Cilium NetworkPolicy)。

ポリシー適用後に両側から測定しました(2026-08-29)。バックエンド → VMの8899番ポートは通過し、実習用Pod → VMの8899番ポートは6秒のタイムアウトでブロックされました。同じネームスペースの 他の学生のVM から来る接続も、このデフォルト拒否に引っかかります。

2つ目の層:VMごとに異なるトークン

エージェントは、VMごとに異なるトークンが一致するリクエストのみを受け付けます(定数時間比較)。このトークンは 学生にとって秘密ではありません。 学生は自分のVMのrootであるため、ユニットファイルを読み取れば見えてしまいます。防ぎたいのは学生ではなく、 ネットワークポリシーに不備があった際に到達してしまった別のPodです。ポリシーが第1層、トークンが第2層です。1つのVMのトークンを知っていても、他のVMには使えません。

トークンはVMのアノテーションではなく、cloud-init Secretに配置します。アノテーションは kubectl get vm -o yaml そのまま一括でコピーされ、その出力内容を人々が互いに貼り付けてしまうためです。

2. 出る方向:インターネットのみ

VM実習ではaptでパッケージを取得し、k3sやkubeadmをインストールする必要があるため、最初は出る方向の通信を許可していました。しかし、許可された範囲が広すぎました。必要なのは インターネットであって、 自宅のネットワークではありません。当時、ゲスト内のrootがアクセスできた場所は、共有ストレージ、イメージレジストリ、ソースリポジトリとデプロイツール、ルーターの管理画面、Kubernetes API、内部AIサービスでした。

現在は、実習用Podと同じ形状の出る方向のポリシーを適用しています。

適用した日(2026-09-05)にゲスト内から直接測定しました。パブリックなインストールスクリプトのアドレスは200を返し、内部リポジトリ・レジストリ・ルーター・Kubernetes API・内部AIサービスはすべて3秒のタイムアウトになりました。同じポリシーの条件下で、10個のVM実習の正解検証(apt・k3s・Docker・バインドマウント実習)がすべて通過しました。

「学生VMはクラスターに手を出せない」の最大の理由がこのポリシーです。ゲストにはKubernetesのサービスアカウントトークンがなく(VMにサービスアカウントボリュームをマウントしません)、トークンがあったとしてもAPIサーバーのアドレスへの経路がブロックされています。

セッション専用ネットワーク:同じセッションのVM同士のみ

3ノードのkubeadm実習では、VM同士が通信する必要があります。セッションごとにポリシーを1つ作成し、 同じセッションのVM同士でのみ トンネル(UDP 4789)を開きます。実測において、他のセッションのVMがそのトンネルに割り込もうとしてUDP 4789を10回送信したところ、相手のゲストでキャプチャされたパケットは0個であり、ポリシーエンジンによって破棄されたという記録が残っています(2026-09-24)。

このポリシーはバックエンドが作成および削除するため、バックエンドにはネットワークポリシーの作成権限が必要です。しかし、 RBACはcreateを名前で絞り込むことができません(Kubernetes RBACドキュメント:新規オブジェクトの名前は権限を評価する時点で決まっていない場合がある)。権限さえ与えれば、バックエンドはどのような形状のポリシーでも作成可能であり、前述のデフォルト拒否ポリシーを削除してしまうことも可能です。攻撃者でなくても、セレクターを誤って書いたコード1行がデフォルト拒否を削除すれば、すべての学生VMの8899番ポートが開き、その事故はいかなるエラーも発生させません。そのため、APIサーバーのAdmission Policy(ValidatingAdmissionPolicy)により、バックエンドからのリクエストを絞り込んで3つの事項を強制します。名前はセッションポリシーの形式の1つのみ、修正は拒否、構造も1つ(同じセッションのラベル、UDP 4789が1つ)です。定義に誤りがある場合は、遮断する方向に失敗します(Fail-closed)。

3. バックエンドの権限:使わない文は閉じる

VMネームスペースはvirt-launcherの特権ゆえに、Podセキュリティ基準を最も緩く設定しています(第2回)。したがって、このネームスペースでは Podを作成できる権限がそのままノードのrootへの扉になります。そのため、バックエンドのサービスアカウントのRoleは次のように構成されています。

できること 理由
VM・VMIの作成・読み取り・削除、開始・停止 セッションの主たる目的
VMIのconsoleサブリソース 学生のターミナル(第1回 ⑨・⑩)
Secret・サービスの作成・読み取り・削除 cloud-init Secret、3ノードセッションの隣接ノード名
Podの読み取り、ログの読み取り ステータス確認
セッションネットワークポリシーの作成・削除 上記第2節、形状はAdmission Policyで固定
PVCの 読み取り、ゴールデンの名前12個のみ ゴールデン判定(第4回)
できないこと 理由
Podの作成 特権ネームスペースにおいてノードのrootへ通じる扉。2026-09-24にマルチVMセッション権限を追加した際、使っていなかったこの扉を同時に閉じた
PVCの作成・削除、DataVolume ゴールデンはオペレーターが作成する。バックエンドが削除できると、回収ミス1つですべてのベースが消滅する
PVCの一覧表示 ゴールデンの名前がルールで決まっているため、総なめにする必要がない

PVC名で絞り込んだ理由

ゴールデンディスクを最初に導入した際、PVCの読み取り権限は名前を指定せず「このネームスペースのPVC get」となっていました。2026-10-14のセキュリティ監査において、それではネームスペース内の すべて のPVC(オペレーター用VMのディスクを含む)を読み取ることができると指摘されました。必要なのはゴールデンのみであるため、 resourceNames によりゴールデンの名前12個(OS 6種 × サイズ 2種)のみに絞り込みました。

- apiGroups: [""]
  resources: ["persistentvolumeclaims"]
  verbs: ["get"]
  resourceNames: [golden-ubuntu-24.04-10gi, golden-ubuntu-24.04-20gi, …]   # 12개

resourceNames は名前を指定するリクエスト(get・update・delete等)には有効ですが、createやdeletecollectionは絞り込めず、list・watchを絞り込むにはクライアント側で名前のフィールドセレクターを付与する必要があります(RBACドキュメント)。そのため、「名前のルール1つ + get1つ」というゴールデンの設計がこの絞り込みを可能にしました。OSを追加する場合はこのリストにも追加する必要がありますが、仮に漏れてもそのOSは読み取りが403になり、コンテナディスクとして起動するだけです(第4回の代替策)。リポジトリのリストとバックエンドの名前規則が食い違っていればテストが検知します。

執筆時点でクラスターに適用されているRoleを読み取ったところ、PVCのルールはまだありませんでした。ゴールデンはまだ有効化されておらず、この権限はゴールデンを有効化する際に合わせて適用すると記録されています。その間にゴールデン実習が追加されても、読み取りが403になるためコンテナディスクとして起動します。ブロックされることなく 安全側(遮断)に 倒れる順序になっています。

ゴールデンを学生が変更できるのか

できません。ベースイメージはvirt-launcher Podに 読み取り専用で マウントされ(第4回)、ゲストから見えるのはベースと書き込みレイヤーを合わせた1つのvirtioディスクのみです。ゲスト内のrootがどれだけ dd を行おうとも、書き込みは自身の書き込みレイヤーに向かいます。ベースファイル自体はゲストのアドレス空間の外側、Podのファイルシステム上に存在します。

4. コンソール:セッションの持ち主のみに

学生用ターミナルのWebSocketを開く際、バックエンドは3つの点を確認します。リクエストがTT Labの画面から来たものか(他のサイトが学生のCookieを利用してrootシェルを開く経路を防ぐ)、Cookieのユーザーがそのセッションの持ち主か、セッションが実行中かです。他人のセッションや存在しないセッションは同じコードで拒否し、区別させません。コンソールサブリソース自体にアクセスできるKubernetesのアイデンティティはバックエンドのサービスアカウントのみです。

5. 複数の層を1行に

層 防御する内容 実測
入ってくるポリシー バックエンド以外はVMに到達できない 実習用Pod → 8899 タイムアウト
採点トークン ポリシーに不備があった際に到達したPodがコマンドを実行できない —
出るポリシー VMが内部ネットワーク・クラスターAPIに到達できない 内部アドレスすべて3秒タイムアウト
セッションネットワークポリシー 他のセッションのVMが自分のセッションネットワークに割り込めない 割り込み試行10回、パケット0個
RBAC + Admission Policy バックエンドコードのミスがノードroot·デフォルトポリシー削除·ゴールデン削除に発展しない —
コンソールの所有者確認 他人のセッションシェルを開けない —

6. 残された隙間

TTの一言。 VM内でrootであることは問題ありません。重要なのは、そのrootが外部の何にアクセスできるかです。入ってくる経路はバックエンド一つ、出て行く経路はインターネットのみ、バックエンドも必ず必要な名前だけ!そして「リポジトリにある」と「クラスタに適用されている」は違う意味なので、直接叩いて確かめてみてください。

参考

ドキュメントはすべて2026-10-16に閲覧しました。

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます