ルートレスコンテナという「最後の防衛線」:権限分離がサーバーを救う理由
現場でインシデント対応をしていると、ふと背筋が寒くなる瞬間がある。それは「Webアプリケーションの脆弱性から、いとも簡単にホストOSのルート権限を奪取された痕跡」を見たときだ。
多くのエンジニアは「コンテナを使っているから安全だ」と信じ込んでいる。だが、標準的なDocker構成のままなら、コンテナ内の root ユーザーは、ホストOS上の root ユーザーと実質的に同義だ。もしアプリが Command Injection などの脆弱性を突かれれば、攻撃者はコンテナという檻をやすやすと破り、ホストの /etc/shadow を読み取り、カーネルモジュールを挿入してシステムを掌握する。
これを防ぐための切り札が「ルートレスモード(Rootless Mode)」だ。今日は、概念論ではなく、明日から現場で使える実装レベルの知見を共有しよう。
—
1. 攻撃者が狙う「コンテナの境界線」の盲点
攻撃者がルート権限を得るための典型的なPoC(概念実証)の流れを理解しておいてほしい。
1. 初期侵入: Webアプリの脆弱性(例: eval() 実行やRCE)を通じてコンテナ内へ。
2. コンテナ内探索: whoami を実行し、root であることを確認。
3. 脱獄(Escape): ホストの /var/run/docker.sock がマウントされている場合、それを利用してホスト側のコンテナを操作したり、--privileged モードの脆弱性を突いてデバイスファイルをマウントし、ホストのディスクを直接読み書きする。
この攻撃を無力化するのが「ルートレスモード」だ。これが有効であれば、万が一コンテナが乗っ取られても、攻撃者の権限は「ホスト上の非特権ユーザー」に制限され、システム全体への影響は極めて小さくなる。
—
2. Podmanによるルートレス環境の構築
Dockerもルートレスをサポートしているが、設計思想からルートレス前提で動く Podman を私は推奨する。まずは、非特権ユーザーでコンテナを動かすための設定を見ていこう。
ユーザー名前空間の設定
ルートレスで実行するには、ユーザーがサブID(subuid, subgid)を持っている必要がある。
# ユーザーにサブIDを割り当て(rootで実行)
usermod --add-subuids 100000-165535 myuser
usermod --add-subgids 100000-165535 myuser
# 設定反映を確認
grep myuser /etc/subuid
この設定により、コンテナ内の root は、ホストOS上では 100000 などのただの非特権ユーザーとしてマッピングされる。これだけで、コンテナからホストの機密ファイルへの直接アクセスは物理的に不可能になる。
—
3. 実践:セキュアなコンテナ起動設定(systemd連携)
単に動かすだけでは不十分だ。運用を考慮し、再起動時にも安全に立ち上がるようにする。以下は、ユーザー権限で動作するPodmanコンテナのサービスファイル例だ。
# /home/myuser/.config/systemd/user/container-web.service
[Unit]
Description=Podman Web App Container
After=network.target
[Service]
Restart=always
# コンテナの起動コマンド(--read-onlyでファイルシステムを保護)
ExecStart=/usr/bin/podman run --rm --name web-app \
--read-only \
--tmpfs /tmp \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
-p 8080:80 \
my-web-app:latest
[Install]
WantedBy=default.target
この設定のポイント
--read-only: アプリケーションの動作に必要な領域以外を書き込み不可にする。攻撃者がバックドアを仕込むのを防ぐ。--cap-drop ALL: Linuxカーネルの全権限を剥奪する。必要なNET_BIND_SERVICE(80番ポート等を利用するための権限)以外は徹底的に絞るのが鉄則だ。--tmpfs /tmp: テンポラリ領域をメモリ上に限定し、ディスクへの永続化を防ぐ。
—
4. アプリケーションコード側での防御(PHPの例)
インフラ層を固めても、アプリが脆弱であれば話にならない。例えば、ファイルアップロード機能で <?php system($_GET['cmd']); ?> のようなコードがサーバーに保存されるのを防ぐ必要がある。
<?php
// ファイルアップロードのバリデーション(一部抜粋)
$allowed_types = ['image/jpeg', 'image/png'];
$file_type = mime_content_type($_FILES['upload']['tmp_name']);
if (!in_array($file_type, $allowed_types)) {
// ログに記録し、即座に終了する
error_log("不正なファイルアップロード試行: " . $_FILES['upload']['name']);
die("Security Error: Invalid file type.");
}
// ファイル名をランダムハッシュ化し、Web公開ディレクトリの外に保存する
$safe_name = bin2hex(random_bytes(16));
move_uploaded_file($_FILES['upload']['tmp_name'], '/var/www/uploads/' . $safe_name);
?>
—
最後に:防御の深層心理
ルートレスモードは魔法の杖ではない。「多層防御」の一部に過ぎない。しかし、この「境界線を厳格に引く」という意識があるかどうかが、重大なインシデントを「単なるアラート」で済ませられるか、それとも「全サーバーのOS再インストール」という悪夢を見るかの分かれ道になる。
明日出社したら、まずは今動いているコンテナが root で動いていないか、docker ps を叩いて確認してほしい。もしそこにあるなら、今すぐルートレスへの移行計画を立てること。それが、プロのエンジニアとしての最低限のたしなみだ。
何か技術的な壁にぶつかったら、また聞きに来てくれ。理論武装と実装の泥臭い積み重ねこそが、我々を守る最強の盾になるのだから。
コメント