「eBPFなしでコンテナを監視するのは、目隠しでモグラたたきをするようなものだ。」
シンプルなコンテナセキュリティツールのつもりが、カーネルパニック・親プロセスの嘘・eBPFの容赦のない厳格さと向き合う数週間になった。これはそれを動かすまでの正直な記録だ。
eBPFとは何か
eBPF(Extended Berkeley Packet Filter)はカーネルのトレースポイントにプログラムをアタッチし、サンドボックス環境で実行できる仕組みだ。実際にできること:
- アプリケーションコードに触れずにリアルタイムでsyscallを監視
- カーネルレベルでのネットワークパケット検査
- コンテナ境界を越えたプロセスツリーの追跡
- 正しく実装された場合の低オーバーヘッド
「安全に」という修飾語は実際に機能する。ベリファイアは安全でないと判断したプログラムを拒否する——つまり学習曲線の大半は拒否されたロードと暗号のようなエラーメッセージだ。
カーネルはすべてを制御する——メモリ、デバイス、セキュリティ。カーネルを壊すとプロセスだけでなくシステム全体が落ちる。カーネルにプローブをアタッチするとき、この違いは非常に重要だ。
解決する問題
現代のコンテナデプロイメントには構造的な監視のギャップがある。コンテナは隔離を提供するが、隔離は監視ではない。
- コンテナの63%が過剰な権限で実行されている
- Kubernetesクラスター1つあたり平均14のエスケープルート
- 37%の組織がコンテナブレークアウトをリアルタイムで検出できない
目標:カーネルレベルでコンテナプロセスを監視し、エスケープパターンを検出し、ブレークアウトがインシデントになる前にアラートを出す。
アーキテクチャ
SEC("tracepoint/syscalls/sys_enter_execve")
int monitor_execve(struct pt_regs *ctx) {
u64 pid_tgid = bpf_get_current_pid_tgid();
u32 pid = pid_tgid >> 32;
if (!is_containerized(pid)) {
return 0;
}
struct event_t event = {};
event.pid = pid;
event.timestamp = bpf_ktime_get_ns();
bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
&event, sizeof(event));
return 0;
}
システムフロー:
-
プローブのアタッチ —
execve、open、connectsyscallを監視 - コンテナフィルタリング — cgroupパスを読み取ってコンテナプロセスを識別
- ふるまい分析 — 既知のエスケープパターンと比較
- ユーザースペースへのアラート — デーモンがログ記録・集約・アラートを処理
実際に起きたこと
カーネルバージョンの要件
$ uname -r
5.4.0-100-generic
必要なeBPF機能にはカーネル5.8以上が必要だった。私は5.4にいた。
正しい手順:まずVMでアップグレードをテストし、すべて検証してから本番で実施。そうした。うまくいった——しかし時間コストは現実だ。VMのステップを飛ばせばひどい目に遭う。
ビルド依存関係(エラーを一つずつ発見した)
$ make
fatal error: linux/bpf.h: No such file or directory
これらを最初にすべてインストールしろ:
libbpf-devlinux-headers-$(uname -r)clangllvmlibelf-dev
私のように一つずつコンパイラエラーで発見するな。
コンテナ検出:最初の試み
// これは全く機能しない。
bool is_containerized(u32 pid) {
return pid > 1000;
}
PIDの範囲はコンテナのメンバーシップについて何も教えてくれない。正しいアプローチ:
bool is_containerized(u32 pid) {
char cgroup_path[256];
snprintf(cgroup_path, sizeof(cgroup_path),
"/proc/%d/cgroup", pid);
return contains_container_indicator(cgroup_path);
}
cgroupファイルを解析し、docker、containerd、kubepods を確認する。コードは増えるが実際に機能する。
記録する価値のあるエラー
ヘッダーの欠落
error: implicit declaration of function 'getppid'
2時間のデバッグ。#include <unistd.h> の欠落。ヘッダーをインクルードしろ。全部。最初から。
ディレクトリの欠落
FileNotFoundError: [Errno 2] No such file or directory: '/var/log/ebpf-monitor/'
import os
os.makedirs('/var/log/ebpf-monitor/', exist_ok=True)
ディレクトリは自分では作られない。別のプロジェクトでもこの教訓を学び続けている。
親プロセスの嘘
Parent PID: 1
Actual parent: containerd-shim (PID: 3847)
コンテナプロセスは本質的に欺く。実際にはコンテナランタイムプロセスの3階層深くにネストされているのに、親としてPID 1を報告する。最初に見つけた親を信用するな——ツリー全体を辿れ:
u32 get_real_parent(u32 pid) {
u32 current = pid;
for (int i = 0; i < 10; i++) {
u32 parent = get_parent_pid(current);
if (is_container_runtime(parent)) {
return parent;
}
current = parent;
}
return 0;
}
カーネルパニック
[ 123.456789] BUG [<ffffffffc0ab1234>] ? my_ebpf_prog+0x45/0x67 [ebpf_monitor]
[ 123.456790] Kernel panic - not syncing: Fatal exception
eBPFのnullポインタ参照はプログラムをクラッシュさせない。システム全体をクラッシュさせる。 回復可能なエラーではない。マシン全体が落ちる。
ポインタをすべてチェックしろ。一つ残らず:
struct task_struct *task = (struct task_struct *)bpf_get_current_task();
if (!task) {
return 0;
}
char *comm = BPF_CORE_READ(task, comm);
if (!comm) {
return 0;
}
ベリファイアはロード時にこの多くを検出するが、すべてではない。ベリファイアに救われることを期待するな。すべてにnullチェックを入れ、実機に触れる前にVMでテストしろ。
結果
| 脅威の種類 | 検出率 | 誤検知率 |
|---|---|---|
| コンテナエスケープの試み | 94% | 2% |
| 権限昇格 | 89% | 5% |
| 不審なネットワーク活動 | 91% | 3% |
| ファイルシステムの改ざん | 87% | 4% |
パフォーマンスオーバーヘッド:CPU約2%、デーモンに15MB + eBPFプログラムごとに8KB、監視対象syscallあたり1ms未満のレイテンシ。
重要な教訓
eBPFは容赦しない。 nullポインタ一つでサーバーを再起動することになる。VMでテストしろ。常に。
コンテナ検出は見た目より難しい。 プロセスは嘘をつく。ランタイムも嘘をつく。最初から堅牢な検出ロジックを構築しろ。
eBPFのドキュメントの大半は自分のカーネルバージョンとは異なるバージョン向けに書かれている。 ブログ記事よりも公式カーネルドキュメントとlibbpfのソースを信頼しろ。正直に言うとこの記事も含めて。
次のステップ
- ふるまいのベースライン化のための軽量MLモデルによる異常検出
- エンタープライズデプロイメント向けのFalco統合
- プロセスツリーとアラート履歴の可視化ダッシュボード
- 重大なアラートをトリガーしたコンテナの自動ブロック
参考資料
- What is eBPF? — 公式入門
- Brendan Gregg's eBPF Guide — パフォーマンス分析
- Cilium eBPF Documentation — 高度なネットワーキング
- Linux Kernel BPF Documentation — 一次資料
ソースコード
完全な実装はGitHubで公開——eBPFプログラム、ユーザースペースデーモン、デプロイ設定を含む。
より詳細なケーススタディはこちらで公開しています。
Elijah Udom (@elijahu_) — ラゴス拠点のインフラ・クラウドエンジニア。AWS、Kubernetes、eBPFセキュリティ、AI/MLインフラ。