コンテナセキュリティの臨界点:ルートレスモード(Rootless Mode)が破壊する攻撃ベクトルの深層
モダンなインフラストラクチャにおいて、コンテナはもはや抽象化されたデプロイの単位ではない。それは、Linuxカーネルが提供する共有リソース上で、複数のプロセスが「隔離されているという幻想」のもとに同居する、極めて密度の高い実行境界である。
多くのセキュリティエンジニアやアーキテクトが「コンテナは隔離されている」という前提に甘んじ、依然として特権(Root)でDockerデーモンを稼働させている。しかし、攻撃者の視点から見れば、ホスト上でRootとして動作するコンテナランタイムは、カーネルの脆弱性を一枚剥ぎ取るだけでシステム全体の完全な支配権(Host Root)を掌握できる、最も魅力的な標的である。
本稿では、コンテナエスケープの低レイヤにおける攻撃メカニズムを紐解き、その究極の防衛策である「ルートレスモード(Rootless Mode)」の内部構造、カーネル空間における権限分離の仕組み、そして導入に伴うトレードオフと実務的な監査手法について、ホワイトハッカーの視点から徹底的に解説する。
—
1. 「コンテナエスケープ」の冷徹な現実:なぜRoot実行は死に至る病なのか
多くの開発現場では、「コンテナ内のRootユーザーは、コンテナ外(ホスト)のRootとは異なる」という誤解がいまだに根強い。だが、コンテナの実体は単なるホスト上のプロセスであり、Linuxカーネルの機能である Namespaces(名前空間)と cgroups(コントロールグループ)によって視界を遮られているに過ぎない。
もしコンテナがホストのRoot(UID 0)とマッピングされた状態で実行されており、かつランタイムやカーネルに脆弱性が存在した場合、攻撃者は容易にその「視界の壁」を突破し、ホストの全権限を掌握する。これを象徴するのが、コンテナ史に刻まれた数々のエスケープ脆弱性である。
CVE-2019-5736:runcの書き換えによるホスト掌握
この脆弱性は、コンテナ内の悪意あるプロセスが、ホスト上で実行される runc(コンテナランタイムのデファクトスタンダード)のバイナリを直接書き換えることを可能にした。
コンテナの起動時、または docker exec の実行時、一時的にホストの runc プロセスがコンテナの名前空間内に侵入する。攻撃者は /proc/self/exe(自身を実行している実行ファイルへのシンボリックリンク)を経由して、ホスト上の runc バイナリへの書き込み権限を取得し、任意の悪意あるペイロードを注入した。次にホスト側でコンテナが起動された瞬間、システム全体のルート権限でそのペイロードが実行される。
CVE-2024-21626:ファイルディスクリプタのリークによる脱獄
より記憶に新しいこの脆弱性では、runc の初期化プロセスにおいて、特定のファイルディスクリプタ(FD)がクローズされずにコンテナ内にリークしていた。
攻撃者は /proc/self/fd/<FD番号> を悪用し、コンテナのファイルシステム境界を越えてホストの物理ファイルシステムに直接アクセスし、/etc/shadow の奪取やホスト側バイナリの書き換えを実行した。
[ホストOS (Root権限で動作するDockerデーモン)]
└── [runc (コンテナ起動処理)]
└── FDのリーク or /proc/self/exe の露出 ── (境界突破) ──> [ホストのファイルシステム/プロセス空間]
▲
│ (コンテナの壁:Namespaceによる隔離)
▼
[コンテナ内 (UID 0 = ホストのUID 0)]
└── 悪意あるプロセス (脆弱性を突き、ホスト側のリソースを直接操作)
これらの脆弱性に共通する根本原因は、「コンテナ内のUID 0が、ホストのUID 0と同一視されていること」、そして「コンテナランタイム自体がホストのRoot権限で動作していること」である。この前提を根本から覆す設計思想こそが、ルートレスモードに他ならない。
—
2. ルートレスモードを支える3つのカーネルテクノロジー
ルートレスモードとは、Dockerデーモン(dockerd)やPodman、そしてそれらが呼び出すコンテナランタイムを、ホスト上の完全な「非特権ユーザー(非Rootユーザー)」の権限下で実行する技術である。
これを実現するために、Linuxカーネルの低レイヤでは主に以下の3つの機構が自律的に協調している。
+-----------------------------------------------------------------------+
| ユーザー空間 (User Space) |
| |
| [非特権ユーザーのセッション (UID: 1000)] |
| └── dockerd / podman |
| └── runc (コンテナ起動) |
| ├─ [User Namespace] ──> コンテナ内では UID: 0 (Root) |
| ├─ [slirp4netns] ───> 非特権でのTCP/IPスタック・仮想化 |
| └─ [fuse-overlayfs] ─> 非特権でのレイヤードファイルシステム |
+-----------------------------------------------------------------------+
| カーネル空間 (Kernel Space) |
| |
| * User Namespaces (非特権での名前空間作成を許可) |
| * subuid / subgid によるUID/GIDの範囲制限 |
+-----------------------------------------------------------------------+
① User Namespaces(ユーザー名前空間)と UID/GID マッピング
ルートレスモードの心臓部である。User Namespacesは、プロセスに対して「特定の名前空間内だけで有効な仮想的なUID/GID」を割り当てる。
これにより、コンテナ内部では完璧なRoot(UID 0)として振る舞い、パッケージのインストール(apt/yum)やシステム設定の変更ができる一方、そのプロセスが一歩コンテナの外(ホストOS)に出ると、単なる一般ユーザー(例:UID 1000)として扱われる。
このマッピングの対応表を定義するのが、/etc/subuid および /etc/subgid である。
② ネットワーク仮想化:slirp4netns と passt
通常、コンテナのネットワークはホスト上に veth ペア(仮想イーサネットデバイス)を作成し、ブリッジに接続することで実現する。しかし、ネットワークインターフェースの作成やルーティングテーブルの操作は、カーネルの CAP_NET_ADMIN 特権を必要とするため、非特権ユーザーには許可されない。
そこでルートレスモードでは、ユーザー空間で動作するネットワークエミュレータである slirp4netns や passt を使用する。これらは、コンテナからのネットワークトラフィックをユーザー空間でキャプッチし、標準的なシステムコール(connect や send など)に変換してホストのネットワークスタックへ中継する。このアプローチにより、特権なしでのコンテナ間通信および外部通信を実現しているが、コンテナ・ホスト間のシステムコールのコンテキストスイッチが発生するため、ネットワークスループットの低下というトレードオフを伴う。
③ ストレージレイヤ:fuse-overlayfs
コンテナは複数のイメージレイヤを重ね合わせて一つのファイルシステムに見せる(OverlayFS)。しかし、標準の overlay マウントは、セキュリティ上の理由からカーネルが非特権ユーザーによる実行を制限している。
ルートレスモードでは、FUSE(Filesystem in Userspace)実装である fuse-overlayfs を用いることで、ユーザー空間で安全にマウント処理を行う。これにより、特権を一切要求せずに、コンテナの高速なコピーオンライト(CoW)ストレージを構築している。
—
3. 実践:セキュアなルートレス環境の完全構築ガイド
ここでは、一般的なLinuxシステム(Ubuntu 22.04 LTS / 24.04 LTS想定)において、Dockerをルートレスモードで堅牢にセットアップする手順を示す。
ステップ1:必要なパッケージとカーネルパラメータの検証
まず、ユーザー名前空間が十分に確保されているか、およびFUSE/slirp4netnsが利用可能かを確認する。
# ユーザー名前空間の最大値を確認(0より大きい、十分な値が必要)
sysctl user.max_user_namespaces
# 必要に応じてカーネルパラメータを永続的に設定
# (多くのモダンなディストリビューションではデフォルトで有効化されている)
sudo sysctl -w user.max_user_namespaces=28633
ステップ2:非特権ユーザーのUID/GID割り当て設定
/etc/subuid および /etc/subgid を編集し、ルートレスコンテナを実行する非特権ユーザー(ここでは sec-user とする)に対して、コンテナ内で利用可能なUID/GIDの範囲(サブUID/GID)を定義する。
# sec-user に対して、ホストのUID 100000から始まる65536個のUIDを割り当てる
# これにより、コンテナ内のUID 0〜65535が、ホストの100000〜165535に安全にマウントされる
sudo usermod --add-subuids 100000-165535 sec-user
sudo usermod --add-subgids 100000-165535 sec-user
設定が正しく反映されているか確認する。
cat /etc/subuid
# 出力例: sec-user:100000:65536
cat /etc/subgid
# 出力例: sec-user:100000:65536
ステップ3:ルートレスDockerのインストール(非特権ユーザーコンテキスト)
ここからは、特権を持たない一般ユーザー(sec-user)としてログインして作業を行う。
# 非特権ユーザーにスイッチ
su - sec-user
# Docker公式が提供するルートレス用セットアップスクリプトを実行
# (このスクリプトは内部で docker-ce-rootless-extras を展開し、ユーザー空間の systemd に登録する)
curl -fsSL https://get.docker.com/rootless | sh
インストールが完了すると、環境変数の設定を促すメッセージが表示される。これを ~/.bashrc や ~/.zshrc に追記する。
# ~/.bashrc に以下を追記
export PATH=/home/sec-user/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
# 設定を即時反映
source ~/.bashrc
ステップ4:systemdによるデーモンのライフサイクル管理
ルートレスDockerは、システム全体の systemd(PID 1)ではなく、ユーザー空間の systemd --user インスタンスによって制御される。
# ルートレスDockerデーモンを起動
systemctl --user start docker
# システム起動時に自動実行されるよう有効化
systemctl --user enable docker
# ステータスの健全性を確認
systemctl --user status docker
また、ユーザーがログアウトした後もコンテナをバックグラウンドで稼働させ続ける(Linger機能)ために、以下の設定が必須となる。
# sec-user のセッションをログアウト後も維持する
sudo loginctl enable-linger sec-user
—
4. 防衛アーキテクチャの監査:ルートレスモードにおける「見えざる死角」
ルートレスモードは極めて強力な防御境界を形成するが、万能薬ではない。本番環境に導入するにあたり、セキュリティアーキテクトが認識し、対処しなければならない「特有の制限」と「監査の盲点」が存在する。
1. 特権ポート(1-1023)のバインド制限
Linuxの伝統的なセキュリティ仕様により、1024番未満のポート(例: 80, 443)は特権ユーザー(Root)しかバインドできない。ルートレスコンテナ内でWebサーバーを起動し、ホストのポート80を直接公開しようとすると、Permission denied エラーで失敗する。
【対策】
- リバースプロキシの併用: ホスト上の特権付きロードバランサー(NginxやHAProxy、あるいはクラウドプロバイダのLB)で443ポートを受け、バックエンドのルートレスコンテナ(例: 8080ポートで待機)へトラフィックを転送する。
- sysctlによる制限緩和: 特定のレンジを非特権ユーザーに開放する。
# 非特権ユーザーによるポート80以降のバインドを許可する設定
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80
2. リソース制限(cgroups v2)の監査
cgroups v1環境では、非特権ユーザーによるCPUやメモリのリソース制限(--memory や --cpus の指定)はサポートされていなかった。ルートレス環境でリソース制限を厳密に適用し、DoS攻撃(コンテナの暴走によるホストリソースの枯渇)を防ぐためには、システムが cgroups v2 で動作していることが絶対条件となる。
【監査コマンド】
# cgroups v2 が有効化されているか確認(「cgroup2fs」または「cgroupv2」が表示されればOK)
stat -f /sys/fs/cgroup
3. eBPFやカーネルモジュールとの統合の喪失
コンテナ内で稼働するセキュリティエージェントや、ローレベルのパケットフィルタリングを行うアプリケーション(例: CiliumなどのeBPFを活用したCNI)は、カーネルに直接フックを挿入するために CAP_SYS_ADMIN や CAP_NET_ADMIN を要求する。これらはルートレス環境下では完全に沈黙する。
したがって、ホスト全体の可観測性(Observability)は、コンテナ内からではなく、ホストOS側にデプロイした監査デーモン(auditd やホスト側で動作するeBPFトレーサー)から一元的に捕捉する設計にシフトしなければならない。
—
5. 監査:ルートレスモードが有効に機能しているかを「実証」する
構築したコンテナ環境が、本当にホストのRoot権限を剥奪できているか、ホワイトハッカーの視点からペネトレーションテスト(侵入実験)を模した監査手法で検証する。
検証1:コンテナ内プロセスとホストプロセスのUIDマッピング監査
まず、コンテナ内で意図的にRootとして振る舞うプロセスを起動する。
# コンテナ内でバックグラウンドプロセスをRootユーザーとして実行
docker run -d --name security-audit-test alpine sleep 3600
次に、ホストOS側でこの sleep プロセスがどのUIDで実行されているかをプロセステーブルから追跡する。
# ホスト上で sleep プロセスの実効ユーザー(EUSER)を確認
ps -eo user,pid,comm | grep sleep
【監査の合否判定】
- 不合格 (危険):
root 12345 sleepと表示された場合。コンテナ内のRootがホストのRootと直結しており、コンテナエスケープ時にシステム全体が陥落する。 - 合格 (安全):
sec-user 12345 sleep(または設定した非特権ユーザーのUID)と表示された場合。コンテナ内ではどんなにRootとして振る舞おうとも、ホストカーネルから見ればただの一般ユーザーsec-userに過ぎない。
検証2:ホストデバイスファイルへのアクセス遮断テスト
特権コンテナがもたらす最大の脅威の一つに、ホストの物理ディスクデバイス(/dev/sdaなど)への直接アクセスがある。ルートレスコンテナにおいて、デバイスへの特権的なアクセスがカーネルレベルで拒絶されるかテストする。
# ホストの物理デバイスをコンテナ内にマウントして書き込みを試みる
docker run --rm --device=/dev/sda:/dev/sda alpine sh -c "dd if=/dev/zero of=/dev/sda bs=1M count=1"
【監査の合否判定】
- 合格 (安全):
docker: Error response from daemon: ...またはコンテナ内でPermission deniedが発生し、物理デバイスへの書き込みが完全にブロックされる。
—
結論:多層防御(Defense in Depth)のラストワンマイル
ルートレスモードは、コンテナセキュリティにおける銀の弾丸ではない。しかし、それはシステムを保護するための「最も強固な防潮堤」である。
万が一、アプリケーションにゼロデイのRCE(遠隔コード実行)脆弱性が発見され、コンテナ内のシェルを奪われ、さらにコンテナランタイム(runc)の未知のエスケープ脆弱性を突かれたとしても、ルートレスモードが有効であれば、攻撃者が手にするのはホスト上の「権限のない一介の一般ユーザーのシェル」に過ぎない。
特権を最小化する設計は、セキュアなシステムアーキテクチャの基本原則(Least Privilege)である。コンテナという利便性の高いテクノロジーを扱うからこそ、その足元にあるLinuxカーネルの権限モデルに厳格であり続けること。これこそが、執拗なサイバー脅威からエンタープライズのインフラを死守するための、揺るぎなき防衛思想である。
コメント