背景:なぜSeccompが重要なのか?
コンテナは仮想マシン(VM)ではありません。本質的には、cgroupsとnamespacesによって分離されつつも、ホストと同一のLinuxカーネルを共有して動作するプロセスにすぎません。そのため、Webアプリケーションにリモートコード実行(RCE)の脆弱性が存在する場合、攻撃者はシステムコール(syscall)を介してホストのカーネルと直接やり取りできてしまいます。
現在、x86_64アーキテクチャのLinux OSには450以上のシステムコールが存在します。一般的なNode.jsやGoのAPIサービスがリクエストを処理するのに必要なシステムコールは、せいぜい40〜70個程度です。しかしデフォルトのDocker設定では、特に危険性の高い約44個のシステムコール(rebootやkexec_loadなど)しかブロックされておらず、残る300以上のシステムコールは野放しの状態になっています。
もしbpf()やkeyctl()のような普段使われないシステムコールにカーネルのゼロデイ脆弱性が発見された場合、攻撃者は特権昇格を行ってコンテナから脱出(コンテナブレイクアウト)する可能性があります。そうなれば、ホストサーバー全体が乗っ取られる事態に陥ります。
最も抜本的な解決策は、ブラックリスト方式からホワイトリスト方式(Default Deny)へと移行することです。アプリケーションが必要とするシステムコールのみを許可し、それ以外はすべて遮断します。
環境準備
まず、カーネルとDockerデーモンがSeccompをサポートしているか確認します。
カーネルのコンフィグフラグを確認します:
grep CONFIG_SECCOMP= /boot/config-$(uname -r)
# CONFIG_SECCOMP=y と出力されれば問題ありません
DockerランタイムがSeccompを認識していることを確認します:
docker info | grep -i seccomp
# 期待される出力:
# Security Options:
# seccomp
# Profile: builtin
システムコールの抽出やデバッグを行うため、ステージング環境にstraceとauditdを追加でインストールします:
sudo apt-get update && sudo apt-get install -y strace jq auditd
実施手順
DockerのSeccompプロファイルの構造は、標準的なOCI JSONフォーマットを採用しています。特に注目すべきフィールドは、defaultAction、architectures、そしてsyscalls配列の3点です。
ステップ1:アプリケーションが実際に使用するシステムコール一覧を取得する
アプリケーションに必要なシステムコールを推測に頼って決めるべきではありません。テスト環境でstraceを実行し、アプリの動作を正確に集計しましょう:
# コンテナをテスト実行し、システムコールの統計情報を収集
docker run --rm --cap-add SYS_PTRACE -v $(pwd):/app -w /app python:3.11-slim \
strace -c -f python -c "print('Hello from isolated container')"
実行すると、呼び出されたシステムコールとその実行回数をまとめた一覧(read、write、mmap、execve、brkなど)が出力されます。抜け漏れを防ぐため、テストスイートの実行や自動負荷テストも併せて実施してください。これにより、アプリが高負荷な処理を行う際に発生するシステムコールも網羅できます。
ステップ2:ホワイトリスト形式のSeccompプロファイルを作成する
custom-seccomp.jsonファイルを作成します。最大のポイントはdefaultActionです。ここにSCMP_ACT_ERRNOを設定することで、明示的に定義されていないすべてのシステムコールを拒否します:
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_X86",
"SCMP_ARCH_AARCH64"
],
"syscalls": [
{
"names": [
"accept4",
"arch_prctl",
"brk",
"clock_gettime",
"clone",
"close",
"connect",
"epoll_create1",
"epoll_ctl",
"epoll_pwait",
"execve",
"exit",
"exit_group",
"fcntl",
"fstat",
"futex",
"getcwd",
"getdents64",
"getpid",
"getrandom",
"madvise",
"mmap",
"mprotect",
"munmap",
"nanosleep",
"newfstatat",
"openat",
"poll",
"read",
"readlinkat",
"restart_syscall",
"rt_sigaction",
"rt_sigprocmask",
"rt_sigreturn",
"sched_yield",
"set_robust_list",
"set_tid_address",
"sigaltstack",
"statx",
"write"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
ステップ3:コンテナ実行時にプロファイルを適用する
通常のDocker CLIを使用する場合、--security-optフラグを指定します:
docker run --rm -it \
--security-opt seccomp=/path/to/custom-seccomp.json \
alpine:latest sh
本番環境でDocker Composeを使用する場合は、サービス定義ブロック内に指定します:
services:
api:
image: my-secure-api:1.0
security_opt:
- seccomp:/etc/docker/seccomp/custom-seccomp.json
ports:
- "8080:8080"
違反のテストとモニタリング
ポリシーを厳格化した後は、保護メカニズムが期待通りに機能しているかを検証する必要があります。
1. 不正な操作を実行して動作をテストする
ホワイトリストに含まれていないシステムコールをあえて呼び出してみます。例えば、ホスト名の変更コマンド(sethostnameの呼び出し)を実行します:
docker run --rm \
--security-opt seccomp=/path/to/custom-seccomp.json \
alpine:latest hostname hacker-box
# 期待される結果:
# hostname: sethostname: Operation not permitted
即座にOperation not permittedエラーが発生します。カーネルは、システムメモリへの書き込みが行われる前にプロセスを強制終了しました。
2. Auditdで違反ログを確認する
コンテナが予期せず異常終了した場合、許可されていないシステムコールが呼び出された可能性があります。監査ログ(Audit log)から素早く確認できます:
sudo ausearch -m SECCOMP -ts recent
または、auditログファイルを直接フィルタリングします:
sudo grep "type=SECCOMP" /var/log/audit/audit.log | tail -n 5
ログ内のsyscall=170というフィールドに注目してください。番号170がどの関数に対応するかを調べるには、次のコマンドを実行します:
ausyscall x86_64 170
# 出力: sethostname
ステージング環境でのワンポイントアドバイス:権限不足でアプリが即座にクラッシュするSCMP_ACT_ERRNOの代わりに、"defaultAction": "SCMP_ACT_LOG"(Linux 4.14以降でサポート)を使用することをおすすめします。このモードではアプリが正常に動作し続け、違反したシステムコールのみが監査ログに記録されます。2〜3日間ログを監視して不足しているシステムコールを追加した上で、本番環境へのデプロイ時にSCMP_ACT_ERRNOへ切り替えて厳格化するのが理想的です。
