コンテナの「脱獄」を許さない:AppArmorとSELinuxで構築する鉄壁の防壁
現場で多くのインシデントを見てきた経験から言わせてもらうと、多くのエンジニアが「コンテナは隔離されている」という言葉を過信しすぎている。DockerやKubernetesで動くコンテナは、あくまでホストOSと同じカーネルを共有する「単なるプロセス」に過ぎない。
もし君が開発したアプリケーションの脆弱性を突かれ、RCE(リモートコード実行)を許したとき、攻撃者はそのコンテナを足場にしてホストOSの /etc/shadow を読み取ったり、他のコンテナへ横展開(ラテラルムーブメント)しようと試みるだろう。これが「コンテナ脱獄(Escape)」だ。
今回は、この悪夢を現実のものにしないための、強制アクセス制御(MAC)による要塞化の実践的アプローチを解説する。
—
1. なぜ「設定」だけでは不十分なのか
多くの現場では、Dockerfile で USER を指定したり、read-only ファイルシステムを使ったりする。確かにこれらは有効だが、これだけでは「カーネルレベルでの権限分離」ができていない。
攻撃者がよく使う手法の一つに、ptrace システムコールを用いたデバッグ機能の悪用や、本来読み取る必要のないプロセス情報(/proc)の探索がある。これらを防ぐには、アプリケーションの挙動を厳密に定義し、それ以外の操作をカーネルレベルで拒否する AppArmor や SELinux が必須だ。
—
2. AppArmorを用いたプロファイルによる封じ込め
AppArmorは、パスベースで「どのプロセスが、どのファイルに、何ができるか」を定義する。特定のコンテナ専用のプロファイルを作成し、Docker実行時に適用するのが最も現実的な防衛策だ。
実践:読み取り専用ディレクトリの設定(AppArmor)
例えば、Webアプリケーションが特定のディレクトリ以外には一切書き込めないように制限するプロファイル例を見てほしい。
# /etc/apparmor.d/docker-custom-profile
# このプロファイルはコンテナ内のプロセスが許可されていないアクセスを遮断する
profile docker-custom-profile flags=(attach_disconnected, complain) {
# 基本的なライブラリへのアクセスは許可
/usr/bin/python3 mr,
/lib/** mr,
# 書き込みを許可するのは /tmp のみ
# その他の場所への書き込みは全て拒否される
deny /etc/** w,
deny /home/** w,
/tmp/** rw,
# ネットワークへのアクセスを制限(例:内部APIのみに限定)
network tcp,
}
このファイルをロードした後、Docker実行時に以下のように指定する。
# プロファイルをロード
sudo apparmor_parser -r /etc/apparmor.d/docker-custom-profile
# コンテナ起動時に適用
docker run --security-opt "apparmor=docker-custom-profile" -d my-web-app
—
3. SELinuxによるマルチカテゴリセキュリティ(MCS)
SELinuxは、AppArmorよりもさらに強力なラベルベースの制御を行う。特にKubernetes環境では、各Podに自動的に異なるラベルを割り当てる「MCS」が標準で機能している。
これを手動で制御する場合、最も重要なのは「コンテナプロセスがアクセスできるリソースのラベルを厳格に定義すること」だ。もし、君がクラウド環境でコンテナを動かしているなら、以下のIAMポリシーと組み合わせることで多層防御が完成する。
クラウドIAMでの多層防御(AWSの例)
コンテナがメタデータサービス(IMDS)を通じてホストのIAMロールを盗む攻撃(SSRF経由の権限昇格)を防ぐため、メタデータへのアクセスを遮断するIAMポリシーを適用する。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "sts:AssumeRole",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:SourceVpc": "vpc-0123456789abcdef"
}
}
}
]
}
—
4. 開発者が意識すべき「防御的コーディング」
いくらインフラを堅牢にしても、アプリケーション側で exec() や eval() を不用意に使えば、防御壁は突破される。以下のPythonコードは、OSコマンド注入を防ぐための最小権限の原則に基づいた実装例だ。
import subprocess
import shlex
def get_system_status(user_input):
# 【悪い例】os.system(f"ping {user_input}")
# これだとコマンド注入が可能
# 【良い例】shlex.splitで入力を分割し、リスト形式で渡す
# これにより、サブシェルを経由しないため注入が極めて困難になる
safe_command = shlex.split(f"ping -c 1 {user_input}")
try:
# shell=False を明示的に設定し、悪意ある入力を防ぐ
result = subprocess.run(safe_command, capture_output=True, text=True, shell=False)
return result.stdout
except Exception as e:
return "Internal Error"
—
最後に:セキュリティは「諦めない」こと
「ここまでやる必要があるのか?」と聞かれることがある。答えは「YES」だ。
攻撃者は君のアプリケーションのソースコードを読み、README を読み、そして脆弱な設定の隙を虎視眈々と狙っている。AppArmorやSELinuxの設定は、最初は面倒かもしれない。しかし、一度プロファイルを書いてしまえば、それは君のシステムを24時間守り続ける「最強の門番」になる。
まずは、現在動いているコンテナの dmesg を見てほしい。もしそこに type=1400 のような権限拒否のログが散見されるなら、それは君のシステムが攻撃を防いでいる証拠だ。その感覚を大切に、明日からの運用に役立ててほしい。
コメント