【テクニカル・上級編】 DockerコンテナのSeccompプロファイルによるシステムコール制限 – インフラ・ネットワーク & クラウドセキュリティ防御ガイド

コンテナの皮を被った「剥き出しのホスト」:なぜデフォルトのSeccompでは不十分なのか

インフラストラクチャの現場で数々のインシデントハンドリングを行ってきた経験から言えば、多くのエンジニアが「Dockerを使っているから安全だ」という神話に囚われている。コンテナは仮想マシンではない。名前空間(Namespaces)とコントロールグループ(cgroups)によってプロセスを隔離しているに過ぎず、コンテナ内で稼働するプロセスは、ホストOSのカーネルと直接システムコールをやり取りしている。

ここに根本的なリスクがある。もしWebアプリケーションの脆弱性を突かれ、コンテナ内のroot権限を奪われたとする。その際、攻撃者がホストカーネルに対して任意のシステムコールを発行できたらどうなるか。コンテナの壁など、紙屑のようなものだ。

デフォルトのDockerは、約300種類存在するLinuxのシステムコールのうち、危険とみなされる約44個をブロックするデフォルトのSeccomp(Secure Computing Mode)プロファイル適用している。しかし、この「汎用的なブラックリスト方式」は、攻撃者がゼロデイ脆弱性やカーネルのロジックバグ(例えば、不完全な入力検証や競合状態)を利用して未知のシステムコールや許可されたシステムコールの不正な組み合わせを実行するのを防ぐには、あまりに力不足だ。

真にセキュアなインフラを目指すのであれば、ホワイトリスト方式に基づき、「そのアプリケーションが生存するために絶対必要なシステムコール以外はすべて殺す」という徹底したハーデニング(要塞化)が不可欠となる。

—

Seccompのメカニズムと低レイヤの防御ロジック

Seccompは、Linuxカーネルの機能であり、プロセスが実行できるシステムコールを制限するサンドボックス機構だ。内部的にはBPF(Berkeley Packet Filter)、正確にはcBPF(クラシックBPF)またはeBPFを用いてフィルタリングルールを評価する。

アプリケーションがメモリ上の操作やファイルディスクリプタの操作、ネットワーク通信を行う際、ユーザー空間からカーネル空間へ処理を移行するために syscall 命令が実行される。Seccompは、この瞬間に割り込み、渡されたシステムコールの番号(sys_enter)と引数を検査する。

攻撃者がカーネルのメモリ破壊脆弱性(Heap OverflowやUse-After-Freeなど)を突き、カーネル空間での任意コード実行を目論む際、彼らは特定のシステムコールを踏み台にする。例えば、特権昇格やコンテナエスケープの文脈では、keyctl, add_key, unshare, ptrace, あるいは過剰な権限を持つ bpf システムコールなどが好んで悪用される。

デフォルトのプロファイルではこれらの一部が塞がれているものの、業務アプリの要件に合わせてさらにフットプリントを削ぎ落とし、攻撃対象領域(アタックサーフェス)を極限まで縮小するのがセキュリティアーキテクトの腕の見せ所だ。

—

実践:カスタムSeccompプロファイルの設計と実装

ここでは、一般的なWebアプリケーション(例:Go言語製またはNode.js製のステートレスAPIサーバー)を想定し、必要最低限のシステムコールのみを許可するカスタムSeccompプロファイル(JSON形式)を設計・実装する。

1. カスタムプロファイル(strict-seccomp.json)の作成

以下の設定は、一般的なコンテナランタイムが起動時に必要とする最低限のシステムコール(プロセス終了、メモリ割り当て、基本的なI/O等)を許可しつつ、危険なシステムコールを拒否するホワイトリストのベースラインだ。

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "defaultErrnoRet": 1,
  "architectures": [
    "SCMP_ARCH_X86_64",
    "SCMP_ARCH_AARCH64"
  ],
  "syscalls": [
    {
      "names": [
        "accept",
        "accept4",
        "arch_prctl",
        "bind",
        "brk",
        "capget",
        "capset",
        "chdir",
        "clone",
        "close",
        "connect",
        "epoll_create1",
        "epoll_ctl",
        "epoll_pwait",
        "execve",
        "exit",
        "exit_group",
        "faccessat",
        "faccessat2",
        "fadvise64",
        "fchdir",
        "fchmod",
        "fchown",
        "fcntl",
        "fstat",
        "fstatfs",
        "fsync",
        "ftruncate",
        "futex",
        "getcwd",
        "getdents64",
        "getegid",
        "geteuid",
        "getgid",
        "getpeername",
        "getpid",
        "getppid",
        "getsockname",
        "getsockopt",
        "gettid",
        "getuid",
        "ioctl",
        "listen",
        "lseek",
        "lstat",
        "madvise",
        "mmap",
        "mprotect",
        "munmap",
        "nanosleep",
        "openat",
        "pipe",
        "poll",
        "prctl",
        "pread64",
        "pselect6,プロセスの待機とファイルディスクリプタ監視",
        "read",
        "readlink",
        "recvfrom",
        "recvmsg",
        "rt_sigaction",
        "rt_sigpending",
        "rt_sigprocmask",
        "rt_sigreturn",
        "rt_sigsuspend",
        "sched_getaffinity",
        "sched_yield",
        "sendto",
        "set_robust_list",
        "set_tid_address",
        "setsockopt",
        "shutdown",
        "sigaltstack",
        "socket",
        "stat",
        "statfs",
        "sysinfo",
        "tgkill",
        "time",
        "times",
        "umask",
        "uname",
        "unlinkat",
        "utimensat",
        "wait4",
        "write",
        "writev"
      ],
      "action": "SCMP_ACT_ALLOW",
      "args": [],
      "comment": "Webアプリケーションの実行に必要最低限のシステムコールのみをホワイトリスト方式で許可"
    }
  ]
}

この設定では、defaultAction に SCMP_ACT_ERRNO(拒否してエラーを返す)を指定し、明示的に許可されたリスト(SCMP_ACT_ALLOW)以外のすべてのシステムコールを容赦なくブロックする。

2. Dockerへの適用方法

作成したプロファイルは、Dockerの起動時またはコンテナの実行時に --security-opt オプションを指定して適用する。

# カスタムSeccompプロファイルを使用してコンテナをデプロイ
docker run --rm \
  --security-opt seccomp=./strict-seccomp.json \
  -p 8080:8080 \
  my-secure-api-server:latest

Kubernetes環境(ContainerdやCRI-O)の場合は、Podのセキュリティコンテキスト(SecurityContext)や、ノード側のリッチなSeccompポリシー(SeccompProfile)として定義し、オーケストレーション層から強制適用する必要がある。

—

現場での監査とトラブルシューティング:AUDITログの解析

厳格なSeccompプロファイルを導入した際によくある失敗が、アプリケーションのフレームワークやランタイム(JVMやGoのランタイム、PythonのC拡張など)が、予期せぬタイミングでマイナーなシステムコールを呼び出し、突然プロセスが異常終了(Operation not permitted や Bad system call)を起こすケースだ。

これをデバッグするためには、Linuxの監査サブシステム(auditd)を活用し、カーネルがどのシステムコールをブロックしたのかをリアルタイムでキャッチする必要がある。

監査ログの確認手順

ホストOS側で auditd が稼働している場合、Seccompによってブロックされたイベントは /var/log/audit/audit.log もしくは journalctl に記録される。

# auditログからseccompに起因する拒否ログをリアルタイムで抽出する
sudo ausearch -m SECCOMP --interpret

ログ出力の例:

type=SECCOMP msg=audit(1698123456.789:12345): auid=425 uid=1000 gid=1000 ses=1 pid=4567 comm="node" exe="/usr/local/bin/node" sig=31 arch=c000003e syscall=101 compat=0 ip=0x7f9a8b7c6d5e code=0x00000000

ここで注目すべきは syscall=101 という数値だ。これは x86_64 アーキテクチャにおけるシステムコールの番号を示している(例えば 101 は ptrace に該当する)。この番号を基に、どのプロセスが何の目的でそのシステムコールを求めたのかを逆引きし、プロファイルに安全に追加するか、あるいはアプリケーション側の実装を修正してその処理を排除するかの判断を下す。

—

チーフホワイトハッカーからの提言:多層防御の極意

Seccompによるシステムコールの制限は、決して「これさえ入れれば万全」という銀の弾丸ではない。しかし、コンテナセキュリティにおける「最後の砦(ディープ・ディフェンス・レイヤ)」として機能する。

悪意ある攻撃者がリモートコード実行(RCE)の足がかりを得たとしても、特権昇格に必要なシステムコールがカーネルレベルで握りつぶされていれば、ホストOSの根幹を揺るがす致命的な侵害(コンテナエスケープ)を防ぎきることができる。

面倒くさい、あるいは動かなくなるリスクがあるからといって、デフォルトの緩い設定のまま本番環境にコンテナを放り込むのは、鍵の壊れた玄関ドアで夜を明かすようなものだ。アプリケーションの挙動を徹底的にプロファイリングし、ゼロ・トラストの思想に基づいたカスタムSeccompプロファイルをビルド・CI/CDパイプラインに組み込むこと。それこそが、プロフェッショナルなインフラエンジニアおよびセキュリティアーキテクトに求められる責務である。

コメント

タイトルとURLをコピーしました