現場のエンジニア諸君。今日も「動けばいい」という甘い誘惑と戦っていることと思う。
コンテナ技術は確かに便利だ。しかし、DockerやKubernetesでアプリケーションを動かす際、ホストOSとの境界線を「単なる名前空間の分離」と高を括っているなら、それは玄関の鍵を開けたまま外出するようなものだ。
今日は、コンテナが乗っ取られた際の「最後の一線」であるシステムコール制限(SeccompとAppArmor)について、実務的な防衛術を伝授する。
—
1. なぜ「システムコール」が狙われるのか?
攻撃者がコンテナ内に侵入した際、彼らが最初に行うのは「ホストOSへのエスケープ(脱獄)」だ。コンテナ内のプロセスは、カーネルの機能を呼び出すためにシステムコールを発行する。もし、コンテナが本来必要としない ptrace や mount、あるいは古い脆弱性が残る keyctl などを実行できれば、ホストOSのカーネルへ直接攻撃を仕掛けられる。
教科書的な防御は「rootで動かすな」だが、現場ではレガシーなライブラリの都合で権限を落とせないこともあるだろう。そんな時、「許可されたシステムコール以外は全て殺す(Default Deny)」という設定が、あなたのシステムを救うことになる。
2. Seccompによるシステムコール・フィルタリング
Seccomp(Secure Computing Mode)は、プロセスが発行できるシステムコールを制限するカーネル機能だ。
実践:最小権限のSeccompプロファイル
Dockerのデフォルトプロファイルは甘い。まずは、不要な特権操作を削ぎ落としたカスタムプロファイルを作成しよう。
profile.json (以下を保存して利用せよ)
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": ["SCMP_ARCH_X86_64"],
"syscalls": [
{
"names": ["read", "write", "exit", "futex", "epoll_wait"],
"action": "SCMP_ACT_ALLOW"
}
]
}
SCMP_ACT_ERRNO: 許可されていないコールにはエラーを返す(プロセスを殺さず、攻撃の痕跡をログに残す)。- 注意:
readやwriteはアプリ実行の最小単位。ここを削ると何も動かない。
適用方法:
docker run --security-opt seccomp=profile.json my-app:latest
—
3. AppArmorによるパスアクセス制御
Seccompが「何ができるか」を制御するなら、AppArmorは「どこに触れられるか」を制御する。特に、/proc や /sys といったホストの機密情報へのアクセスを遮断するのに有効だ。
実践:ファイルシステムをガチガチに固める
以下は、特定のディレクトリ以外への書き込みを一切禁止するプロファイルの例だ。
etc/apparmor.d/containers/my-app
#include <tunables/global>
profile my-app-profile flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
# 読み取り専用で許可するディレクトリ
/usr/bin/python3 mr,
/app/** r,
# 書き込みを許可する唯一の場所
/tmp/logs/ w,
# ホストの重要なファイルへのアクセスを拒否
deny /etc/shadow w,
deny /proc/sys/kernel/core_pattern w,
}
—
4. 現場で使える「セキュアな設計」の鉄則
インシデントハンドリングの現場で見ると、侵入後に curl や wget を使ってマルウェアをダウンロードするケースが後を絶たない。これを防ぐには、そもそもコンテナからインターネットへの外向き通信を制限するのが一番だ。
アプリ側での安全な実装(Python例)
もしAPIサーバーを構築しているなら、osモジュールやsubprocessモジュールを直接叩かせない設計を徹底すること。
import os
import subprocess
# 悪い例: ユーザー入力を直接コマンドとして実行する(絶対NG)
# subprocess.run(f"ls {user_input}", shell=True)
# 良い例: 必要な処理をPython標準ライブラリで完結させる
import pathlib
def list_files(directory):
# パスを制限し、ディレクトリトラバーサルを防ぐ
base_dir = pathlib.Path("/app/data").resolve()
target_dir = (base_dir / directory).resolve()
if not target_dir.startswith(base_dir):
raise PermissionError("不正なアクセスです")
return [f.name for f in target_dir.iterdir()]
—
最後に:セキュリティは「多層」で守る
SeccompやAppArmorの設定は、運用負荷が高いと感じるかもしれない。だが、一度プロファイルを作ってしまえば、それは「コード化された防御」となる。
1. 最小権限の原則: コンテナをrootで動かさない。
2. システムコールのホワイトリスト化: Seccompで不要な道を断つ。
3. ファイルパスの制限: AppArmorでホストへの接触を防ぐ。
これらを組み合わせることで、万が一アプリケーションに脆弱性(RCEなど)が見つかっても、攻撃者が「ホストのシェルを奪う」ことは不可能になる。
セキュリティは、完璧を目指すのではなく、「攻撃者のコストを跳ね上げ、諦めさせること」が肝要だ。今日のこの設定が、数ヶ月後の君たちの深夜の緊急対応を救うことになるはずだ。健闘を祈る。
コメント