おい、みんな、ちょっといいか?
最近、コンテナ技術が当たり前になって、開発も運用もスピードが格段に上がった。それは素晴らしいことだ。ただ、その裏で、俺たちが忘れちゃいけないセキュリティの「盲点」があるんだ。特に、コンテナランタイムのセキュリティ、中でも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を実行 アプリケーションを動かし、各種機能をテストする ログファイルを確認 ポイント: SeccompプロファイルはJSON形式で記述する。主な要素は以下の通りだ。 ここでは、非常にシンプルなWebアプリケーション(例: 静的ファイル配信や基本的なAPI応答)を想定した「最小限のSeccompプロファイル」の例を示す。 { 重要な注意点: Seccompプロファイルの準備ができたら、Dockerコマンドで簡単に適用できる。 Seccompプロファイルを指定してコンテナを起動 確認 このコマンド一つで、コンテナは指定されたSeccompプロファイルに従ってシステムコールを実行するようになる。もしアプリケーションが許可されていないシステムコールを呼び出そうとすると、 Kubernetesでも同様に、PodのSecurityContextを使ってSeccompプロファイルを適用できる。Kubernetes 1.19以降では、 apiVersion: v1 image: my-app-image:latest # コンテナレベルでのSecurityContextも設定可能 (ここでは最低限の例) add: Kubernetesでの適用手順: 1. ノードにSeccompプロファイルを配置: # 例: ノードにファイルをコピー (実際はCI/CDや設定管理ツールで自動化) 2. Podをデプロイ: kubectl apply -f my-secure-pod.yaml Kubernetes 1.25+での推奨: apiVersion: v1 image: my-app-image:latest ただし、Dockerの Seccompは強力なツールだが、導入すれば終わりというわけではない。運用フェーズでの注意点も理解しておこう。 アプリケーションは進化する。新しい機能が追加されれば、それまで使っていなかったシステムコールが必要になる可能性もある。Seccompプロファイルを導入した後は、以下のような運用を心がけよう。 ゼロからプロファイルを作成するのは骨が折れる。最初は既存のプロファイルを参考にしたり、利用したりするのも賢い選択だ。 本番環境にSeccompプロファイルを適用する前に、必ず開発・テスト環境で十分な検証を行うこと。 Seccompはインフラレイヤーのセキュリティだが、アプリケーション開発者も無関係ではない。 「おい、みんな、今日の話、どうだった?」 Seccompプロファイルは、コンテナ化されたアプリケーションのセキュリティを強化するための強力なツールだ。しかし、これだけで全てが解決するわけじゃない。WAFによるWebアプリケーション層の防御、ネットワークACLによる通信制限、IAMによる認証・認可、そして脆弱性スキャンやコードレビューによる早期発見。これら全てが組み合わさって初めて、堅牢なシステムが構築される。いわゆる「レイヤードディフェンス」ってやつだな。 Seccompは、RCEを許してしまった後の「最後の砦」として、攻撃者の行動を大幅に制限し、被害を最小限に食い止める可能性を秘めている。デフォルトのDockerプロファイルに頼りきりではなく、自分のアプリケーションに合わせたカスタムプロファイルを作成し、適用すること。これは、我々エンジニアが本番環境のセキュリティを本気で考える上で、避けては通れない道だ。 最初は手間がかかるかもしれない。でもな、実際にインシデントが起きて、ホストOSまで侵入された時の被害を考えたら、この手間は決して無駄じゃない。むしろ、将来の大きなコストと信頼の失墜を防ぐための、最善の投資だと言える。 今日の話を参考に、ぜひ自分のチームでもSeccompプロファイルの導入を検討してみてくれ。もし途中で困ったことがあれば、いつでも相談に乗るからな。共に、よりセキュアな世界を築いていこうじゃないか!
strace -f -o /tmp/php-fpm_syscalls.log -p
Webアクセス、DB接続、ファイル操作、外部API呼び出しなど、一通り実行する
docker cp my-app:/tmp/syscalls.log .
less syscalls.logsyscalls.logには、アプリケーションが呼び出したシステムコールがずらっと並んでいるはずだ。このログを元に、許可すべきシステムコールのリストを作成していく。
ステップ2: カスタム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: システムコールの引数に基づく条件付け(高度な設定)。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プロファイルの適用
–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” ] のような出力があればOKdefaultActionで指定した通り、エラーが返されるか、プロセスが終了する。ステップ4: KubernetesでのSeccompプロファイルの適用
seccompProfileフィールドがベータ版として利用可能になり、より使いやすくなった。my-secure-pod.yaml:
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:
ports:
securityContext:
allowPrivilegeEscalation: false # 特権昇格を禁止
readOnlyRootFilesystem: true # ルートファイルシステムを読み取り専用に
capabilities:
drop:
Seccompプロファイル(minimal-webapp-seccomp.json)をKubernetesクラスタの各ノードの特定のディレクトリ(例: /var/lib/kubelet/seccomp/profiles/)に配置する。これはDaemonSetなどを使って自動化するのが一般的だ。
scp minimal-webapp-seccomp.json user@your-node:/var/lib/kubelet/seccomp/profiles/
上記のYAMLファイルを適用する。
Kubernetes 1.25以降では、Seccompのデフォルト設定が強化され、特別な設定なしにDockerのdefaultプロファイルが適用されるRuntimeDefaultが推奨されている。もし、カスタムプロファイルではなく、デフォルトで十分な場合はtype: RuntimeDefaultを指定するだけで良い。
kind: Pod
metadata:
name: my-secure-pod-runtime-default
spec:
securityContext:
seccompProfile:
type: RuntimeDefault # DockerのデフォルトSeccompプロファイルを適用
containers:
ports:
defaultプロファイルは、汎用性を考慮して多くのシステムコールを許可しているため、より厳格なセキュリティを求める場合は、やはりカスタムプロファイル(Localhost)を検討すべきだろう。Seccomp運用のベストプラクティスと運用上の注意点
1. 継続的なモニタリングとプロファイルの更新
defaultActionでSCMP_ACT_ERRNOやSCMP_ACT_KILLを設定した場合、ブロックされたシステムコールはカーネルログ(dmesgやjournalctl -k)に出力される。これらのログを監視し、アプリケーションが正しく動作しているにも関わらずシステムコールがブロックされている場合は、プロファイルの更新を検討する。2. 既存のプロファイルの活用とコミュニティの知見
dropしていくというアプローチも考えられる。3. テスト環境での十分な検証
4. アプリケーション開発者がSeccompを意識すべき点
setuidビットを持つヘルパープロセスを使用)など、最小限に留める。まとめ:セキュリティはレイヤードディフェンス
コメント