【実務・中級編】コンテナランタイムのセキュリティ:Seccompプロファイルによるシステムコール制限 – アプリケーションセキュリティ & 安全な開発防御ガイド

おい、みんな、ちょっといいか?

最近、コンテナ技術が当たり前になって、開発も運用もスピードが格段に上がった。それは素晴らしいことだ。ただ、その裏で、俺たちが忘れちゃいけないセキュリティの「盲点」があるんだ。特に、コンテナランタイムのセキュリティ、中でもSeccomp(セックコンプ)プロファイルによるシステムコール制限は、まさにその盲点の一つと言えるだろう。

多くのチームが「コンテナだから分離されてるし大丈夫」と思いがちだが、もしコンテナ内でRCE(Remote Code Execution)を許してしまったら、次に攻撃者が狙うのは何か? そう、カーネルのエクスプロイトや、ホストへのコンテナエスケープだ。そんな時に、最後の砦として機能するのがSeccompなんだ。

今日は、そのSeccompについて、お前らが日々の開発や運用で「これなら使える!」と思えるような、泥臭い現場の知見と、コピペで使える具体的な設定を交えて徹底的に解説する。教科書的な話はもう飽きたろ? 俺たちの手で、本当に堅牢なシステムを作っていこうじゃないか。

—

なぜ今、Seccompプロファイルが「最後の砦」なのか?

コンテナは、その軽量さと高速性から現代の開発に不可欠な存在になった。DockerやKubernetesがその最たる例だ。しかし、コンテナはVM(仮想マシン)のように完全に分離されているわけじゃない。ホストOSのカーネルを共有している以上、カーネルレベルの脆弱性を突かれたり、特定のシステムコールが悪用されたりすれば、コンテナからホストへ、あるいは他のコンテナへの影響が及ぶ可能性がある。

想像してみてくれ。お前らが苦労して作ったWebアプリケーションにXSSやSQLインジェクションの脆弱性があって、そこからRCEが達成されたとする。攻撃者は次に何を試みると思う? 彼らは、コンテナ内での特権昇格、あるいはホストOSへのアクセス、つまり「コンテナエスケープ」を狙うんだ。

この時、攻撃者が使う手口の一つが、危険なシステムコールを直接叩くことだ。例えば、mountでホストのファイルシステムをマウントしようとしたり、ptraceで他のプロセスをデバッグして情報を抜き取ろうとしたり、unshareで新しい名前空間を作成して隔離を破ろうとしたりする。

ここでSeccompが輝く。Seccompは、コンテナ内のプロセスが「どのシステムコールを使えるか」を厳密に定義できる。たとえコンテナ内でroot権限でプロセスが動いていたとしても、Seccompプロファイルで危険なシステムコールをブロックしていれば、攻撃者の企みを水際で食い止めることができるんだ。まるで、カーネルへの最後の関門、セキュリティの「最終防衛ライン」だな。

攻撃者の視点:システムコールが悪用されるシナリオ

具体的なPoCを羅列するのは避けたいが、攻撃者がどのような意図でシステムコールを悪用しようとするかを理解することは、防御策を考える上で非常に重要だ。

1. コンテナエスケープと特権昇格

  • mount: 最も古典的な手口の一つ。コンテナ内でRCEに成功した後、ホストの/(ルートディレクトリ)をコンテナ内にマウントしようとする。成功すれば、ホストのファイルシステムに自由にアクセスできるようになり、設定ファイルの改竄やSSHキーの窃取、マルウェアの配置など、やりたい放題だ。
  • ptrace: プロセスを追跡・操作するシステムコール。他のプロセスのメモリ空間を読み書きしたり、レジスタの値を変更したりできる。これにより、コンテナ内の他のプロセスから機密情報を抜き取ったり、挙動を改変したりする。特定の条件下では、コンテナエスケープの足がかりにもなりうる。
  • unshare: 新しい名前空間(namespace)を作成する。コンテナはLinuxカーネルの名前空間機能を使って隔離されているが、unshareを悪用することで、新しい名前空間を作り出し、ホストの特定の名前空間から「抜け出す」ような挙動を引き起こし、コンテナの隔離を破ろうとする。
  • kexec_load: 新しいカーネルをロードして実行する。これは非常に強力なシステムコールで、悪用されればホストOSのカーネルを乗っ取ることが可能になる。通常はコンテナで必要ないはずだが、デフォルトのSeccompプロファイルでは許可されている場合もあるため注意が必要だ。

これらのシステムコールは、通常のWebアプリケーションが動作する上ではほとんど必要ない。だからこそ、これらを制限することが、セキュリティを劇的に向上させることに繋がるんだ。

実践!Seccompプロファイルの作成と適用

さて、ここからが本番だ。SeccompプロファイルはJSON形式で記述する。DockerもKubernetesも、このJSONファイルを指定するだけで適用できる。

ステップ1: アプリケーションが必要なシステムコールを特定する

「最小権限の原則」に従って、アプリケーションが必要なシステムコールだけを許可するのが理想だ。だが、いきなり完璧なプロファイルを作るのは難しい。最初は「何が必要か」を把握することから始めよう。

コンテナ内でアプリケーションを動かしながら、straceを使って必要なシステムコールをトレースするのが一番手っ取り早い。

まず、通常のコンテナでアプリケーションを起動
docker run -d –name my-app my-app-image

起動中のコンテナ内でstraceを実行
-f: 子プロセスもトレース
-e trace=… : 特定のシステムコールのみをトレース (オプション)
-o output.log: 出力をファイルに保存
アプリケーションのメインプロセスPIDを特定し、そのプロセスにアタッチ
docker exec -it my-app sh -c “apt-get update && apt-get install -y strace && strace -f -o /tmp/syscalls.log <アプリケーションのメインコマンド>”

例: Apache + PHP-FPMの場合
まず、ApacheやPHP-FPMが起動しているPIDを特定する
ps aux | grep apache
ps aux | grep php-fpm

その後、例えばPHP-FPMのPIDに対してstraceを実行
strace -f -o /tmp/php-fpm_syscalls.log -p

アプリケーションを動かし、各種機能をテストする
Webアクセス、DB接続、ファイル操作、外部API呼び出しなど、一通り実行する

ログファイルを確認
docker cp my-app:/tmp/syscalls.log .
less syscalls.log

syscalls.logには、アプリケーションが呼び出したシステムコールがずらっと並んでいるはずだ。このログを元に、許可すべきシステムコールのリストを作成していく。

ポイント:

  • 開発環境やテスト環境で十分な負荷テストや機能テストを行い、網羅的にシステムコールを収集する。
  • ミドルウェア(Webサーバ、DBクライアントなど)が使用するシステムコールも考慮する。
  • 最初は少し広めに許可し、徐々に絞り込んでいくアプローチも有効。

ステップ2: カスタムSeccompプロファイル(JSON)の作成

SeccompプロファイルはJSON形式で記述する。主な要素は以下の通りだ。

  • defaultAction: デフォルトのアクション。SCMP_ACT_ALLOW (すべて許可), SCMP_ACT_ERRNO (エラーを返す), SCMP_ACT_KILL (プロセスを終了させる) などがある。通常はSCMP_ACT_ERRNO または SCMP_ACT_KILL を指定し、必要なものだけをallowするホワイトリスト方式が推奨される。
  • architectures: プロファイルが適用されるCPUアーキテクチャ。通常はx86_64など。
  • syscalls: システムコールごとのルールを定義する配列。
  • names: システムコール名の配列(例: ["read", "write", "openat"])。
  • action: そのシステムコールに対するアクション(SCMP_ACT_ALLOWなど)。
  • args: システムコールの引数に基づく条件付け(高度な設定)。

ここでは、非常にシンプルなWebアプリケーション(例: 静的ファイル配信や基本的なAPI応答)を想定した「最小限のSeccompプロファイル」の例を示す。

minimal-webapp-seccomp.json:

{
“defaultAction”: “SCMP_ACT_ERRNO”, // デフォルトではシステムコールを拒否し、EPERMエラーを返す
“architectures”: [
“SCMP_ARCH_X86_64”, // x86_64アーキテクチャに適用
“SCMP_ARCH_AARCH64” // ARM64アーキテクチャにも適用(M1 Macなどで開発している場合)
],
“syscalls”: [
// 基本的なファイルI/O
{ “names”: [“read”, “write”, “open”, “close”, “access”, “stat”, “fstat”, “lstat”, “pread64”, “pwrite64”, “readlink”, “getdents64”, “openat”], “action”: “SCMP_ACT_ALLOW” },
// プロセス管理
{ “names”: [“exit”, “exit_group”, “getpid”, “getppid”, “gettid”, “getuid”, “geteuid”, “getgid”, “getegid”, “capget”, “capset”, “prctl”], “action”: “SCMP_ACT_ALLOW” },
// メモリ管理
{ “names”: [“mmap”, “munmap”, “mprotect”, “brk”], “action”: “SCMP_ACT_ALLOW” },
// ネットワークI/O(Webサーバーに必要な最小限)
{ “names”: [“socket”, “bind”, “listen”, “accept”, “connect”, “sendto”, “recvfrom”, “sendmsg”, “recvmsg”, “shutdown”], “action”: “SCMP_ACT_ALLOW” },
// 時間関連
{ “names”: [“gettimeofday”, “clock_gettime”, “nanosleep”], “action”: “SCMP_ACT_ALLOW” },
// シグナル処理
{ “names”: [“rt_sigaction”, “rt_sigprocmask”, “rt_sigreturn”], “action”: “SCMP_ACT_ALLOW” },
// その他一般的に必要とされるもの
{ “names”: [“ioctl”, “fcntl”, “pipe”, “pipe2”, “dup”, “dup2”, “dup3”, “set_tid_address”, “set_robust_list”, “futex”, “epoll_create”, “epoll_ctl”, “epoll_wait”], “action”: “SCMP_ACT_ALLOW” },
// デバッグやシステム情報取得(ログ出力など)
{ “names”: [“uname”, “sysinfo”], “action”: “SCMP_ACT_ALLOW” },
// アプリケーションによっては必要(例: スレッド作成)
{ “names”: [“clone”, “arch_prctl”], “action”: “SCMP_ACT_ALLOW” },
// Dockerのデフォルトプロファイルに含まれるが、一般的なWebアプリには不要な危険なシステムコールをここで拒否することも可能
// たとえば、デフォルトアクションを”SCMP_ACT_ERRNO”にしているので、ここに記載がないものは全て拒否される。
// 明示的に拒否したい場合は以下のように記述するが、defaultActionがERRNOなら不要
// { “names”: [“mount”, “ptrace”, “unshare”, “kexec_load”], “action”: “SCMP_ACT_ERRNO” }
]
}

重要な注意点:
このプロファイルはあくまで例だ。あなたのアプリケーションがPHP-FPMを使うのか、Node.jsを使うのか、Goで書かれているのか、あるいはデータベースに接続するのか、外部APIを叩くのかによって、必要なシステムコールは大きく変わる。必ずstraceで確認し、アプリケーションの動作に必要な最小限のシステムコールを許可するように調整してくれ。

ステップ3: DockerでのSeccompプロファイルの適用

Seccompプロファイルの準備ができたら、Dockerコマンドで簡単に適用できる。

Seccompプロファイルを指定してコンテナを起動
–security-opt seccomp=<プロファイルファイルへのパス>
docker run -d \
–name my-secure-app \
–security-opt seccomp=./minimal-webapp-seccomp.json \
my-app-image:latest

確認
docker inspect <コンテナIDまたは名前>
“SecurityOpt”: [ “seccomp=./minimal-webapp-seccomp.json” ] のような出力があればOK

このコマンド一つで、コンテナは指定されたSeccompプロファイルに従ってシステムコールを実行するようになる。もしアプリケーションが許可されていないシステムコールを呼び出そうとすると、defaultActionで指定した通り、エラーが返されるか、プロセスが終了する。

ステップ4: KubernetesでのSeccompプロファイルの適用

Kubernetesでも同様に、PodのSecurityContextを使ってSeccompプロファイルを適用できる。Kubernetes 1.19以降では、seccompProfileフィールドがベータ版として利用可能になり、より使いやすくなった。

my-secure-pod.yaml:

apiVersion: v1
kind: Pod
metadata:
name: my-secure-pod
spec:
securityContext:
# Seccompプロファイルを適用するための設定
seccompProfile:
# typeはRuntimeDefault, Localhost, Unconfinedのいずれかを指定
# RuntimeDefault: DockerのデフォルトSeccompプロファイルを適用 (Kubernetes 1.25+で推奨)
# Localhost: ノード上の特定のパスに配置されたカスタムプロファイルを適用
# Unconfined: Seccompプロファイルを適用しない (非推奨)
type: Localhost
# Localhostを選択した場合、プロファイルファイルへのパスを指定
# ノード上の /var/lib/kubelet/seccomp/profiles/ ディレクトリ以下に配置するのが一般的
# この例では、設定済みの minimal-webapp-seccomp.json を参照
localhostProfile: profiles/minimal-webapp-seccomp.json
containers:

  • name: my-app-container

image: my-app-image:latest
ports:

  • containerPort: 80

# コンテナレベルでのSecurityContextも設定可能 (ここでは最低限の例)
securityContext:
allowPrivilegeEscalation: false # 特権昇格を禁止
readOnlyRootFilesystem: true # ルートファイルシステムを読み取り専用に
capabilities:
drop:

  • ALL # 可能な限り全てのLinux Capabilitiesを削除

add:

  • NET_BIND_SERVICE # 1024番未満のポートにバインドするために必要など、最低限のCapabilityのみ追加

Kubernetesでの適用手順:

1. ノードにSeccompプロファイルを配置:
Seccompプロファイル(minimal-webapp-seccomp.json)をKubernetesクラスタの各ノードの特定のディレクトリ(例: /var/lib/kubelet/seccomp/profiles/)に配置する。これはDaemonSetなどを使って自動化するのが一般的だ。

# 例: ノードにファイルをコピー (実際はCI/CDや設定管理ツールで自動化)
scp minimal-webapp-seccomp.json user@your-node:/var/lib/kubelet/seccomp/profiles/

2. Podをデプロイ:
上記のYAMLファイルを適用する。

kubectl apply -f my-secure-pod.yaml

Kubernetes 1.25+での推奨:
Kubernetes 1.25以降では、Seccompのデフォルト設定が強化され、特別な設定なしにDockerのdefaultプロファイルが適用されるRuntimeDefaultが推奨されている。もし、カスタムプロファイルではなく、デフォルトで十分な場合はtype: RuntimeDefaultを指定するだけで良い。

apiVersion: v1
kind: Pod
metadata:
name: my-secure-pod-runtime-default
spec:
securityContext:
seccompProfile:
type: RuntimeDefault # DockerのデフォルトSeccompプロファイルを適用
containers:

  • name: my-app-container

image: my-app-image:latest
ports:

  • containerPort: 80

ただし、Dockerのdefaultプロファイルは、汎用性を考慮して多くのシステムコールを許可しているため、より厳格なセキュリティを求める場合は、やはりカスタムプロファイル(Localhost)を検討すべきだろう。

Seccomp運用のベストプラクティスと運用上の注意点

Seccompは強力なツールだが、導入すれば終わりというわけではない。運用フェーズでの注意点も理解しておこう。

1. 継続的なモニタリングとプロファイルの更新

アプリケーションは進化する。新しい機能が追加されれば、それまで使っていなかったシステムコールが必要になる可能性もある。Seccompプロファイルを導入した後は、以下のような運用を心がけよう。

  • ログの監視: defaultActionでSCMP_ACT_ERRNOやSCMP_ACT_KILLを設定した場合、ブロックされたシステムコールはカーネルログ(dmesgやjournalctl -k)に出力される。これらのログを監視し、アプリケーションが正しく動作しているにも関わらずシステムコールがブロックされている場合は、プロファイルの更新を検討する。
  • 定期的なレビュー: アプリケーションのバージョンアップやミドルウェアの変更時に、Seccompプロファイルが現状に即しているかレビューする。
  • Canary Deployment: 新しいSeccompプロファイルを適用する際は、少数のPodにのみ適用し、問題がないことを確認してから全体に展開するCanary Deploymentの手法が有効だ。

2. 既存のプロファイルの活用とコミュニティの知見

ゼロからプロファイルを作成するのは骨が折れる。最初は既存のプロファイルを参考にしたり、利用したりするのも賢い選択だ。

  • Dockerのデフォルトプロファイル: Dockerが提供しているデフォルトのSeccompプロファイルは、多くの一般的なコンテナワークロードに対応している。これをベースに、不要なシステムコールをdropしていくというアプローチも考えられる。
  • CIS Kubernetes Benchmark: CIS (Center for Internet Security) が発行しているKubernetesのベンチマークには、セキュリティ強化のための推奨事項が含まれている。Seccompに関する項目も要確認だ。
  • OSSプロジェクトのプロファイル: 類似のアプリケーションやミドルウェアをOSSとして提供しているプロジェクトが、Seccompプロファイルを公開している場合がある。それらを参考にすることもできるだろう。

3. テスト環境での十分な検証

本番環境にSeccompプロファイルを適用する前に、必ず開発・テスト環境で十分な検証を行うこと。

  • 機能テスト: アプリケーションの全ての機能が正しく動作するか。
  • パフォーマンステスト: Seccompのオーバーヘッドは小さいが、念のためパフォーマンステストも実施する。
  • 異常系テスト: 意図的に許可されていないシステムコールを呼び出してみて、期待通りの挙動(エラー、プロセス終了)になるか確認する。

4. アプリケーション開発者がSeccompを意識すべき点

Seccompはインフラレイヤーのセキュリティだが、アプリケーション開発者も無関係ではない。

  • 最小権限での実行: アプリケーションは、可能な限り最小権限で動作するように設計する。root権限が必要な処理は、個別に権限昇格を行う(例: setuidビットを持つヘルパープロセスを使用)など、最小限に留める。
  • 特権的な操作の回避: アプリケーションが直接カーネルと対話するような、特権的なシステムコールを呼び出す設計は可能な限り避ける。例えば、ファイルシステムへの直接のデバイスアクセスや、ネットワークインターフェースの直接操作などだ。
  • コンテナ化を前提とした設計: コンテナの隔離メカニズムを理解し、その中で安全に動作するよう設計する。例えば、一時ファイルの保存場所、ログの出力先、設定ファイルの読み込み方法など、ホストOSのファイルシステムに依存しないようにする。

まとめ:セキュリティはレイヤードディフェンス

「おい、みんな、今日の話、どうだった?」

Seccompプロファイルは、コンテナ化されたアプリケーションのセキュリティを強化するための強力なツールだ。しかし、これだけで全てが解決するわけじゃない。WAFによるWebアプリケーション層の防御、ネットワークACLによる通信制限、IAMによる認証・認可、そして脆弱性スキャンやコードレビューによる早期発見。これら全てが組み合わさって初めて、堅牢なシステムが構築される。いわゆる「レイヤードディフェンス」ってやつだな。

Seccompは、RCEを許してしまった後の「最後の砦」として、攻撃者の行動を大幅に制限し、被害を最小限に食い止める可能性を秘めている。デフォルトのDockerプロファイルに頼りきりではなく、自分のアプリケーションに合わせたカスタムプロファイルを作成し、適用すること。これは、我々エンジニアが本番環境のセキュリティを本気で考える上で、避けては通れない道だ。

最初は手間がかかるかもしれない。でもな、実際にインシデントが起きて、ホストOSまで侵入された時の被害を考えたら、この手間は決して無駄じゃない。むしろ、将来の大きなコストと信頼の失墜を防ぐための、最善の投資だと言える。

今日の話を参考に、ぜひ自分のチームでもSeccompプロファイルの導入を検討してみてくれ。もし途中で困ったことがあれば、いつでも相談に乗るからな。共に、よりセキュアな世界を築いていこうじゃないか!

コメント

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