コンテナ脱獄の深淵:runc/crunの脆弱性と「境界線」を死守するハーデニング術
現場で「コンテナは隔離されているから安全だ」と盲信しているエンジニアは、一度その認識を捨てたほうがいい。かつて猛威を振るった CVE-2019-5736 を覚えているか? あの脆弱性は、コンテナ内の悪意あるプロセスがホスト側の runc バイナリを上書きすることで、ホストOSのルート権限を奪取するという、まさに「脱獄(Container Escape)」の教科書のような攻撃だった。
インフラ担当者が「とりあえず動けばいい」とランタイムのアップデートを怠り、デフォルト設定のままコンテナを走らせることは、施錠を忘れた玄関に「どうぞお入りください」と看板を掲げているに等しい。今日は、泥臭いインシデント現場で培った「コンテナランタイムを要塞化する鉄則」を伝授する。
—
1. なぜ「ランタイムの追従」が命綱なのか
コンテナランタイム(runc や crun)は、OSのカーネル機能(Namespaceやcgroups)を操作して隔離環境を作る「門番」だ。しかし、この門番自体にバグがあれば、どんなに強固なファイアウォールも意味をなさない。
特に注意すべきは、ホストのバイナリをマウントしているパスの扱いだ。古いバージョンの runc では、コンテナ内部からホストのバイナリファイルを書き込み可能にする余地があった。これを防ぐ唯一の防御策は、「ランタイムをOSのパッケージ管理に依存させず、常に最新のバイナリへ追従させること」そして「コンテナに必要以上の特権を与えないこと」に尽きる。
—
2. コンテナ脱獄を封じるための「鉄壁の設定」
コンテナのハーデニングにおいて、最も重要なのは「権限の最小化」だ。特に CAP_SYS_ADMIN や --privileged モードなど、安易にフラグを立てていないだろうか?
以下の設定は、Docker/Containerd環境において、最低限守るべき「境界線」を定義する daemon.json の最適解だ。
{
// 信頼できないイメージの実行を制限する(セキュアなレジストリのみ許可)
"allow-nondistributable-artifacts": [],
// デフォルトでユーザー名前空間を有効化し、ホストとコンテナのUIDを分離する
// これにより、コンテナ内でrootであってもホストのrootにはなれない
"userns-remap": "default",
// コンテナ間通信の制限(デフォルトのブリッジネットワークを隔離)
"icc": false,
// ログの最大サイズを制限し、ディスク枯渇によるDoS攻撃を防ぐ
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
—
3. 実践:Podmanを用いた「ルートレス・コンテナ」への移行
もし君たちのチームがまだ「ルート権限でコンテナを動かす」という古い慣習に縛られているなら、Podman への移行を強く推奨する。Podmanは、そもそもroot権限を必要としないアーキテクチャ(Rootless)で設計されており、万が一コンテナが突破されても、その先には「一般ユーザー」という壁しか存在しない。
以下は、docker-compose から移行する際の注意点を含めた、堅牢な定義ファイル例だ。
# docker-compose.yml のような定義をPodman向けに考えるなら
services:
web-app:
image: my-app:latest
# ルート権限を一切持たせない
user: "1000:1000"
# 不要なカーネル機能(Capabilities)をすべて剥奪し、必要なものだけを付与
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
# ホストOSのバイナリへの書き込みを物理的に不可能にする読み取り専用マウント
read_only: true
tmpfs:
- /tmp
- /run
—
4. 開発者が守るべき「コードレベル」の防壁
インフラがどれだけ強固でも、Webアプリケーション自体が RCE(リモートコード実行) を許せば、攻撃者はコンテナ内で足場を築く。PHPやPythonでファイルを扱う際、パス・トラバーサルを許さない実装を徹底すること。
以下は、ファイルアップロード機能において、コンテナ内での不正な操作を抑制するPHPのサンプルコードだ。
<?php
// ファイル名のバリデーション(ディレクトリトラバーサル防止)
$filename = basename($_FILES['upload']['name']);
$uploadDir = '/var/www/uploads/';
$targetPath = $uploadDir . $filename;
// 拡張子ホワイトリストによるフィルタリング
$allowedExtensions = ['jpg', 'png', 'pdf'];
$fileExtension = pathinfo($targetPath, PATHINFO_EXTENSION);
if (!in_array($fileExtension, $allowedExtensions)) {
die("不正なファイル形式です。");
}
// コンテナ内で実行権限を持たないディレクトリに保存する
if (move_uploaded_file($_FILES['upload']['tmp_name'], $targetPath)) {
// 成功時の処理
} else {
// エラーログ出力
error_log("Upload failed: " . $filename);
}
?>
—
最後に:セキュリティは「設定」ではなく「文化」
セキュリティ設定は一度やって終わりではない。OSのカーネルアップデート、ランタイムのパッチ、そしてアプリケーションの脆弱性診断。これらを「面倒な作業」と捉えるか、「システムを延命させるための不可欠な投資」と捉えるかで、エンジニアとしての価値は大きく変わる。
私の経験上、最も恐ろしいのは「脆弱性そのもの」ではなく、「放置されているという事実を放置すること」だ。今日紹介した設定を、まずはステージング環境の1台から適用してみろ。その「小さな一歩」が、数年後の重大なインシデントを未然に防ぐ鍵になるはずだ。
何か詰まったら、いつでも聞くといい。共に堅牢なシステムを築こう。
コメント