CNCF連載 の4本目です。
はじめに
数年前にクラウドネイティブ注目技術として挙げられたeBPFにかねてよりキャッチアップしたいなと思っていたので、この連載のタイミングでeBPFとその関連プロダクトに入門してみることにしました。
CNCFプロジェクト傘下のeBPFを活用したプロダクトとしてはCilium, Falcoなどが挙げられます。CiliumはKubernetesなどのクラウドネイティブな環境でネットワーク、オブサーバビリティの機能を提供するOSSなのですが、今回はそのいわばサブプロジェクト的な位置づけのセキュリティツールである、Tetragonに触ってみます。
Cilium, Tetragonの開発をメイン行っているIsovalent社は、書籍やハンズオンラボなどで自社の製品・eBPFについての学習リソースを多く提供しています。
https://isovalent.com/resource-library/books
eBPFを学ぶ書籍はいくつかあると思うのですが、今回ブログを書くにあたってはIsovalentが提供しているeBook、Learning eBPFにざっと目を通しました。
eBPF入門
eBPFとはLinuxカーネル内部で高速安全にプログラムを実行する技術です。これにより、ネットワーキング、セキュリティ、アプリケーションのカーネルレベルでの振る舞いを、カーネルをビルドしなおしたりOSの再起動をしたりせずに動的に観測・制御できます。
eBPFそのものの実態は、カーネルのイベントをトリガーとして動作するプログラムとその実行環境です。基本的にC言語で記述してeBPFのバイトコードにコンパイルされ、eBPFのVMで動きます。eBPFバイトコードはカーネルにロードされる前に検証器にチェックされるようになっており、そのおかげでカーネルをクラッシュさせたり脆弱性のもとになるバグを埋め込まず安全に実行できるようです。
eBPFを使ったツールを開発する場合、eBPFプログラムそのものと、それをカーネルのイベントソースにアタッチしてeBPFとデータをやり取りするユーザスペースのコードを書く必要があります。
実際にeBPFのサンプルプログラムを動かしてみましょう。サンプルプログラムにはBCC (BPF Compiler Collection)という、Python/LuaでeBPFを扱うことのできるツールを使います。インストール方法はこちらが参考になります。
次はHello Worldプログラムです。
#!/usr/bin/python3 |
- eBPFプログラムはヒアドキュメントで書かれた部分です。C言語の文法で書かれています。
BPF(text=program)
でeBPFプログラムを渡してBPFオブジェクトを作っていますが、このタイミングでeBPFプログラムがコンパイルされます。get_syscall_fname("execve")
で execveシステムコール(実行可能ファイルが実行される時に呼ばれるシステムコール)に対応するカーネル関数を検索します。b.attach_kprobe(event=syscall, fn_name="hello")
でebpfプログラムを検索したカーネル関数にアタッチします。ここではkprobeという実行中のカーネルに動的に処理を差し込むための仕組みでイベントにアタッチしています。
このPythonスクリプトを実行し、別ターミナルで ls
コマンドを実行すると、ls
が実行される時にexecve()が呼ばれ、それにアタッチされたeBPFプログラムが次のようなトレースを出力します。
b' <...>-2140994 [000] d...1 556700.676560: bpf_trace_printk: Hello World!' |
“Hello World” だけでなく、どのようなプロセスがトレースされているのかを表示できるようにしてみましょう。eBPFプログラムの中からプロセス名を取得するために、bpf_get_current_comm()
というヘルパー関数を使ってみます。
#!/usr/bin/python3 |
このPythonスクリプトを実行した状態で別ターミナルで ls
を実行すると、bash
から ls
が起動したのだということがわかります。
b' <...>-2196911 [000] d...1 567624.022345: bpf_trace_printk: bash' |
今までトレースを出力するために bpf_trace_printk
という関数を使ってきましたが、これは主にデバッグ目的で使うようなもので、PythonのユーザスペースのプログラムとeBPFプログラムの間で情報をやり取りするには、eBPF mapというデータ構造を使います。
eBPF mapとして利用できるデータ構造はいくつかありますが、今回はring bufferを使ってみます。
#!/usr/bin/python3 |
BPF_PERF_OUTPUT
の部分がリングバッファーを作成するマクロです。output
という名前で定義されています。bpf_get_current_pid_tgid()
bpf_get_current_uid_gid()
でイベントをトリガーしたプロセスのプロセスID・UIDを取得し、data_t
にセットします。output.perf_submit
でmapにデータを送ります。print_event()
は バッファにデータが届いたときに呼び出されるコールバック関数です。バッファにはb["output"]
のようにアクセスできます。b["output"].open_perf_buffer
でコールバック関数を登録しています。b.perf_buffer_poll()
でバッファにポーリングしています。
このPythonスクリプトを実行し、別ターミナルでコマンドを実行してみます。
2461142 1000 bash |
トレースされている情報は先ほどと同じですが、Python側のコードで出力を整えられるので見やすくなっていますね。
BPF mapは複数のeBPFプログラム間のデータのやり取りも可能です。
BCC + Pythonは初心者にとっつきやすいですが、実際にはeBPFのコードを実行時に毎回コンパイルするオーバーヘッドがあるだとか、コンパイルするホストと実行するホストが異なるとその差分に影響されてポータビリティが低いといった理由で、現在ではプロダクトの開発に使われていないみたいです。その代わりCO-RE (Compile Once- Run Everywhere)という仕組みをサポートするライブラリを使って実装されているようです。
ここまでくると次から紹介するTetragonの振る舞いやその設定方法がなんとなく理解できるようになります。
Tetragon
TetragonはもともとCilium Enterpriseの機能として提供されていたセキュリティ可観測性ツールで、2022年にOSSとして発表されました。主にKubernetesでの利用が想定されています。
主な機能としては、定義したポリシーに従ってKuberntesクラスター上のコンテナ内で実行されるプロセスのシステムコールやネットワーク関連のイベントをフィルタリングし、ログとして出力するというものです。ポリシーに応じて動的にeBPFプログラムをアタッチし、カーネル空間内で直接フィルタリングしています。
プロセス実行の監視
まずはデモを動かしてどういうことができるのか確かめましょう。kindでクラスターを作り、helmでTetragonをインストールします。
kind create cluster |
次にセキュリティイベントの観測対象となる実験用のPodを作成しておきます。
kubectl run test --image=busybox -- sleep 3600 |
Tetragonではデフォルトでプロセスの実行に対して検知ログを出すようになっています。例えばコンテナでデバッグ用途以外でシェルが起動しているのはいかにも怪しいですが、そのようなイベントを観測できるということです。
Tetragonのログをtailしながら、別のターミナルから先ほど作ったPodで /bin/sh
を動かしてみましょう
kubectl logs -f -n kube-system -l app.kubernetes.io/name=tetragon -c export-stdout |
Tetragonから以下のようなログが出力されたと思います。長いので途中を省略していますが、”binary” を見ると確かに /bin/sh
が実行されていると分かります。そしてPodのMetadataとしてPod名やNamespaceコンテナイメージも出力できています。
{ |
ところで、bpftool
というBPFユーティリティツールを使うと、カーネルにロードされたeBPFプログラムをリストできます。Tetragonをインストールしたノード上でリストしてみると、Tetragonをインストールした直後(ロードされた時刻も表示されるのでそこでわかります)にいくつかeBPFプログラムがロードされているようです。
bpftool prog list |
そのうちの1つ、20671: tracepoint name event_execve tag be83f62b7aed485e gpl
は、その名前から察するに execve
システムコールをトレースしているようです。先ほどのサンプルコードと似ていますね。これらのeBPFプログラムたちがプロセスの起動やexitを監視しているみたいです。
eBPFのソースコードはおそらくこのあたりでしょう。先ほどのサンプルコードと違う部分として、kprobeではなく静的なイベントソースであるTracepointという仕組みにアタッチされていたり、複数のeBPFプログラムが組み合わさっていたりします。
ちなみにTetragonの長いjsonログをパースして必要に応じてフィルタリングして、見やすく表示してくれる tetra
というCLIツールがあります。
kubectl logs -n kube-system -l app.kubernetes.io/name=tetragon -c export-stdout -f | tetra getevents --namespace default -o compact |
ファイルアクセスの監視
別のユースケースとして、コンテナ内のファイルアクセスをトレースしてみましょう。コンテナ内のファイルを書き換えることによって例えばWebコンテンツの改竄をすることが可能になるので、そういったイベントは検知するべきです。
Tetragonでは、TracingPolicyというCRDを作成することでトレースしたいカーネル関数を動的に指定できます。ファイルアクセスをトレースしたい場合、その時に呼ばれるカーネル関数やシステムコールをトレースするTracingPolicyを作るということになります。
ここでは /etc/
ディレクトリ内のファイルを読み書きしている様子をトレースしてみましょう。Tetragonのexamplesとして提供されているmanifestを使います。
apiVersion: cilium.io/v1alpha1 |
kubectl apply -f sys_write_follow_fd_prefix.yaml |
試しに実験用のPod内で /etc/passwd
を編集してみましょう。
kubectl exec -it busybox -- /bin/sh |
Tetragonのログを見るとファイルの open
read
close
をトレースできています。
🚀 process default/busybox /bin/vi /etc/passwd |
/etc/
ディレクトリ以外のファイルへの書き込みはトレースされません。なぜなら、eBPFプログラムがトレーシング対象のカーネル関数の引数として渡されるファイル名を取得し、フィルタリングしているからです。
Tracing Policyの内容の詳しく見て行きましょう。一部を取り出してみました。
spec: |
kprobeというのは先ほども出てきましたが、カーネルの関数に動的に処理を差し込むための仕組みなのでした。spec.krpobes
より下の階層はつぎのような意味です
call
にはトレース対象のカーネル関数を定義します。今回はfd_install
が対象です。この関数はファイルテーブルに新しいファイル記述子を割り当てる関数、、、早い話がファイルオープン時に必ず呼ばれる関数です。この関数にkprobeを使ってeBPFプログラムをアタッチする、ということです。fd_install` の引数の0番目はint型、1番目はfileという構造体であり、これらの引数をトレースに含めます。selectors
以下はフィルタリング条件と、フィルターにマッチしたときの挙動を定義しています。matchPID
- PID Namespace内でpid=1ではないプロセスに対してトレースする(つまりコンテナで動かす本来のプロセスはpid 1なのでトレース対象外で、
kubectl exec
などで実行したプロセスがトレース対象となります)
- PID Namespace内でpid=1ではないプロセスに対してトレースする(つまりコンテナで動かす本来のプロセスはpid 1なのでトレース対象外で、
- matchArgs
- indexの1番目=fileのprefixが
etc
の場合にトレースする。
- indexの1番目=fileのprefixが
matchActions.action: FollowFD
- カーネル関数に渡されたファイル記述子とファイル名をBPF mapに保存する。
他のspec.kprobe
以下の部分も同じように、どの関数にeBPFをアタッチするかを定義しています。
FllowFD
によってBPF mapに保存されたファイル記述子は、他の関数にアタッチされたeBPFからルックアップされます。今回のTracingPolicyだとsys_read()
にアタッチされたeBPFが、関数の引数として渡されるファイル記述子がBPF mapに保存されているものかどうかを参照し、そうであればトレースする、という挙動をとります。
今までトレーシング機能を紹介してきましたが、フィルタリング条件に合致するイベントを検出した際に、プロセスに直接SIGKILLを送出する、といったことも可能です。
終わりに
eBPFとeBPF製品Tetragonに入門にしてみました。Tetragonの親プロジェクトのCiliumでは、eBPFでネットワークを効率化しています。主要クラウドプロバイダーのKubernetesサービスでは、Ciliumが使用できるようになっており、例えばGoogle CloudのGKEではDataplane V2というモードで提供されています。暇があればCilium, eBPF+ネットワークも勉強したいなと思います。
TetragonやBCCの公式ドキュメントのほか、以下のブログを参考にしました。