自己紹介
後藤慶也 (ごとうけいや)
東北大学大学院 理学研究科物理学専攻 修士1年の後藤慶也です。 大学院では酵母における解糖系の振動現象について研究しています。 インフラにおけるセキュリティに興味があり、今回のインターンに参加しました。
小川博徒 (おがわひろと)
東京電機大学 工学部第二部情報通信工学科4年の小川博徒です。
アルバイトでKubernetesに触ったことがありましたが、経験が浅く体系的な知識を身につけたいと思いインターンに参加いたしました。
はじめに
はじめまして、スリーシェイクのSreake 事業部インターン生の後藤慶也と小川博徒です。Sreake 事業部は SRE 技術に強みを持つエンジニアによるコンサルテーションサービスを提供する事業部であり、私たちも SRE 技術の調査と研究を行う目的で2022年10月11日 ~ 24日に開催された短期インターンに参加しました。2週間という期間を使って、eBPF によるコンテナランタイムセキュリティツールの Tetragon の技術検証と運用方法の提案を行いました。以下では、その成果をまとめたいと思います。
コンテナセキュリティとeBPF
DockerやKubernetesを使用してシステムを構築するにあたり、安全な運用を行うためにはコンテナに関するセキュリティが必要不可欠です。
まず最初に取り組むべきことは、イメージの脆弱性に対するセキュリティです。イメージのスキャンを行い、事前に脆弱性を発見することで脆弱性のあるコンテナのデプロイを防止することができます。しかし、たとえスキャン済みのコンテナだったとしても、デプロイ後に脆弱性が発覚するということもあり得るため、イメージのスキャンだけでは不十分です。
そこで重要になるのがコンテナランタイムにおけるセキュリティです。コンテナが実行される本番環境はコンテナライフサイクルにおいて最も期間が長く、運用中にいつ問題が発生するかわかりません。そのため継続してコンテナの動作を監視し、異常が生じた際に迅速に対応するシステムが求められます。
こうしたコンテナランタイムにおける異常な振る舞いを検知するために、Linuxカーネルの機能であるeBPFを利用する方法があります。
本記事は、
前半:eBPFの基礎的な解説と簡単なサンプルの実行(後藤) 後半:kubernates環境へのTetragonのデプロイとその検証結果(小川)
という構成になっています。
eBPFとは
eBPFはLinuxカーネル内の様々なイベントをトリガとして、プログラムを実行できるLinuxの仕組みです。この機能によってユーザはカーネルのソースコードを変更することなく、安全かつ効率的にカーネルの機能を追加・拡張することができます。
プログラムを書く ~ 実装までの流れ
プログラムの書き方
eBPFのコードには、プログラムを実行するトリガとなるイベント(=フック)の指定や、実行されるプログラムの内容を書きます。
eBPFプログラムはバイトコードの形式でLinuxカーネルに読み込まれるので、C言語でコードを書き、バイトコードを作成することが可能です。しかし、bccやlibbpf、AyaなどのeBPFプログラムの作成を補助するツールを使うことでC++、Python、Lua、Rustでより簡潔にeBPFコードを書くことができます。
バイトコードに変換されたeBPFプログラムは、eBPFシステムコールでLinuxカーネルに読み込まれます。
- 補足
- システムコール A system call is a way for programs to interact with the operating system. ― システムコールはプログラムがOSとやりとりをするための方法です。 引用元: geeksforgeeks.org, 「Introduction of System Call」,https://www.geeksforgeeks.org/introduction-of-system-call/
- バイトコード ソフトウェアによって実装される仮想的なコンピュータのために設計された命令コードの体系。 引用元: e-words.jp, 「バイトコード」, https://e-words.jp/w/バイトコード.html
プログラムの検証
Linuxカーネルに読み込まれたeBPFプログラムは、まずeBPF Verifierによって、安全に実行可能か検証されます。
検証する条件の例としては
- eBPFプログラムを読み込むプロセスは要求された権限を持っているか
- プログラムはクラッシュしたり、システムに危害を加えないか
- プログラムは常に最後まで実行されるか?(無限ループして次の処理が保留されないか)
などがあります。
JITコンパイル
eBPF Velifierによって安全に実行可能だと判断されたeBPFプログラムは、次にJIT(Just-in-Time) Compilerによって汎用的なバイトコードから、CPUやメモリなどデバイス固有の条件に最適化されたコードへと変換されます。この変換によって、eBPFプログラムはカーネルモジュールとして読み込まれたコードと同じくらい効率的に実行することができます。
- 補足
- カーネルモジュール オペレーティングシステム(OS)の中核部分であるカーネルに機能を追加するよう設計されたソフトウェア部品 引用元: e-words.jp, 「カーネルモジュール」, https://e-words.jp/w/カーネルモジュール.html
イベントへの紐づけ
JIT CompilerによってコンパイルされたeBPFプログラムは、最終的にフックとして指定されたイベントに紐づけられます。これにより、そのイベントが発生するとeBPFプログラムに書かれた処理が実行されるようになります。
フックできるイベントとしてあらかじめ定義されているものは、システムコールや関数の開始・終了、カーネルのトレースポイント、ネットワークイベントなどがあります。必要なイベントが定義されていない場合は、Kprobeを用いることでカーネル空間の任意の場所をフックに指定できます。ユーザ空間の任意の場所はUprobeを使用することでフックとして指定できます。
- 補足
- Kprobe Kernel probes are a set of tools to collect Linux kernel debugging and performance information. ― Kernel probesはLinuxカーネルのデバッグやパフォーマンスの情報を集めるためのツールセットです。 引用元: documentation.suse.com, 「Kernel Probes」, https://documentation.suse.com/sles/12-SP4/html/SLES-all/cha-tuning-kprobes.html#:~:text=Kernel probes are a set,the system for better performance.
以上がeBPFのコードを書くところからプログラムが実行されるまでの流れになります。次は簡単なeBPFプログラムを実際に書いて実行してみます。
Hello World
eBPF – BCCチュートリアル 編 – Zennを参考に実際にBCCライブラリを使用してC++でeBPFプログラムを書いて実行してみました。
環境
Linux Mint 20.3
必要なパッケージのインストール
以下のコマンドをターミナルで実行し、必要なパッケージを事前に
インストールします。
>$ sudo apt install bpfcc-tools linux-headers-$(uname -r)
$ sudo apt install libbpfcc-dev
ディレクトリ構成
好きな場所で構わないので以下のような構成のディレクトリを作成します。
eBPF_hello_world
├── CMakeLists.txt
├── build
└── src
└── hello_world.cpp
コード
各ファイルの内容は以下の通りです。
hello_world.cpp
#include <bcc/BPF.h>
#include <fstream>
#include <iostream>
#include <string>
const std::string BPF_PROGRAM = R"(
int on_syscall_execve(void* ctx) {
bpf_trace_printk("Hello world by execve call.\\n");
return 0;
}
)";
int main() {
ebpf::BPF bpf;
auto init_res = bpf.init(BPF_PROGRAM);
if (init_res.code() != 0) {
std::cerr << init_res.msg() << std::endl;
return 1;
}
auto fnname = bpf.get_syscall_fnname("execve");
auto attach_res = bpf.attach_kprobe(fnname, "on_syscall_execve");
if (init_res.code() != 0) {
std::cerr << attach_res.msg() << std::endl;
return 1;
}
std::ifstream pipe("/sys/kernel/debug/tracing/trace_pipe");
while (true) {
std::string line;
if (std::getline(pipe, line)) {
std::cout << line << std::endl;
auto detach_res = bpf.detach_kprobe(fnname);
if (init_res.code() != 0) {
std::cerr << detach_res.msg() << std::endl;
return 1;
}
break;
} else {
std::cout << "Waiting for an event." << std::endl;
sleep(1);
}
}
return 0;
}
CmakeLists.txt
cmake_minimum_required(VERSION 3.15)
set(CMAKE_CXX_STANDARD 17)
project("hello_world" LANGUAGES CXX)
find_package(PkgConfig)
pkg_check_modules(LIBBCC REQUIRED IMPORTED_TARGET libbcc)
set(BINARY_NAME hello_world)
add_executable(${BINARY_NAME}
"src/hello_world.cpp"
)
target_include_directories(${BINARY_NAME} PRIVATE "src")
target_include_directories(${BINARY_NAME} PRIVATE PkgConfig::LIBBCC)
target_link_libraries(${BINARY_NAME} PRIVATE bcc)
コードの中身を確認してみみると、実行される処理は以下のように文字列として記述してあります。
const std::string BPF_PROGRAM = R"(
int on_syscall_execve(void* ctx) {
bpf_trace_printk("Hello world by execve call.\\n");
return 0;
}
)";
以下のように新しいプログラムを実行するためのシステムコールであるexecve
をフックに指定し、実行する処理をフックに紐づけています。
auto fnname = bpf.get_syscall_fnname("execve");
auto attach_res = bpf.attach_kprobe(fnname, "on_syscall_execve");
コンパイル
eBPF_hello_world/build/内で以下のコマンドを実行すると、hello_worldという実行ファイルが生成されます。
$ cmake ..
$ make
実行結果
sudo ./hello_world
で実行します。これだけでは何も表示されませんが、別で立ち上げたターミナルでls
コマンドを実行すると、
bash-353877 [006] .... 7814.222355: 0: Hello world by execve call.
という結果が表示されます。
以上がeBPFの基礎的な解説と簡単なサンプルの実行になります。ここからは、実際にeBPFを用いたコンテナランタイムセキュリティの例としてTetragonを取り上げて、Kubernates環境へのデプロイと、その検証結果について書いていきます。
コンテナランタイムセキュリティの応用
Tetragon の概要
コンテナランタイムセキュリティの応用として、eBPFを利用してリアルタイムのイベント監視とランタイムの実行を可能にするTetragonというツールが存在します。
CRD、OPA等を介してセキュリティポリシーを組み込むことで、フックするイベントとイベントの処理を定義することができます。例えば、不正なイベントを検知した際、そのプロセスをkill(SIGKILLにより)することにより、実行される前にイベントが強制終了するといった操作が可能になります。
- OPAについて
Open Policy Agent(OPA)は汎用ポリシーエンジンです。 与えられた構造化データがRegoと呼ばれるポリシー言語で記述されたポリシーを満たしているか判定します。 引用元: Policy as Codeを実現する Open Policy Agent / Rego の紹介, https://tech.isid.co.jp/entry/2021/12/05/Policy_as_Codeを実現する_Open_Policy_Agent_/_Rego_の紹介
なお、TetragonはCilium Enterprise(有料版)の機能の一部を公開したツールになります。
Kubernetes環境だけでなくDocker環境でも利用できるようになっていますが、今回はKubernetes環境にTetragonをデプロイし、検証した結果をお話できればと思います。
Tetragon のデプロイ
環境情報
公式のドキュメントをもとに、以下のようにHelm を用いて、Tetragon を デプロイします。
helm repo add cilium <https://helm.cilium.io>
helm repo update
helm install tetragon cilium/tetragon -n kube-system
Tetragon の Helm Charts を導入すると、DaemonSets と CRD が作成されます。DaemonSets の Pod には、3つのコンテナが含まれ、以下で説明します。
Tetragon Pod で動くコンテナについて
- export-stdout
- TracingPolicy (CRD) をもとに、検知したイベントのログを出力する
- ホスト及びコンテナを検知し、後者の場合はPod 名がログに付与される
- tetragon
- 検知したイベントに対して、TracingPolicyに定義された操作を行う
- tetragon-operator
- Init Container (Pod起動時最初に実行されるコンテナ)
- なので、作成後すぐにterminatedになります
- 何をしているか把握が難しかったため、ソースコードを見てみました
- ソースコードから見るに、「サポートされているk8sのconfigを取得」、「最低限のcapabilitiesを持っているかの確認」、「CRD の有無の確認及び登録」の処理を行なっておりました
- Init Container (Pod起動時最初に実行されるコンテナ)
Tetragon で利用できるAPI リソース(Custom Resource)を確認
$ kubectl api-resources
tracingpolicies cilium.io/v1alpha1 false TracingPolicy
※ Tetragon に関係する cilium.io のもののみ抜粋
TracingPolicy (CRD) を確認
kubectl の出力をみやすくしてくれるプラグイン(kubectl-neat)を導入して確認します
- https://github.com/itaysk/kubectl-neat
- Kubernetes にとって必要で私たちには必要のない情報を省いてくれます
➜ kubectl get tracingpolicies
NAME AGE
deny-privileged-pod-start 3d
※ 詳細をみたいとき
kubectl get tracingpolicies deny-privileged-pod-start -o yaml | kubectl neat
TracingPolicy を反映させる
今回適用したいポリシー
以下は特権コンテナの起動を防ぐ TracingPolocy です。
それぞれのパラメータの意味を追って紹介していきます。
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: "deny-privileged-pod-start"
spec:
kprobes:
# match open fd_install at pod start (pod起動時にfd_installが実行された時にマッチ)
- call: "fd_install"
syscall: false
args:
- index: 0
type: int
- index: 1
type: "file"
selectors:
# match all the namespace PIDs including init (initを含む全てのnamespace PIDにマッチ)
- matchPIDs:
- operator: NotIn
followForks: false
isNamespacePID: true
values:
- 0
# match a process with CAP_SYS_ADMIN (CAP_SYS_ADMINを有するプロセスにマッチ)
matchCapabilities:
- type: Effective
operator: In
values:
- "CAP_SYS_ADMIN"
# match a process with CAP_SYS_ADMIN that gained it later (不明のため後述)
matchCapabilityChanges:
- type: Effective
operator: In
values:
- "CAP_SYS_ADMIN"
# match on containerd-shim binary (containerd-shimバイナリにマッチ)
matchBinaries:
- operator: "In"
values:
- "/usr/bin/containerd-shim"
# terminate the process
matchActions:
- action: Sigkill
パラメータの内容
TetragonのCRD(Tracingpolicies)のdescriptionを参考にしつつ、実際に動かしてみて明らかになったパラメータの役割をまとめます。
kprobes
- 特定の関数を呼び出す
call
- 指定したカーネル関数やシステムコールが実行されたイベントを取得する
syscall
- call で指定した関数がシステムコールであるか否か
args
- call で指定した関数に渡す引数
selectors
- マッチする条件を指定する
- マッチしたイベントに対して、matchActionで操作を指定する
- match〇〇については後述
matchPIDs
- values で指定した pid にマッチした/しなかった場合にイベントを検知する
operator
- valuesに指定したPIDがイベントに含まれている(
In
)か否か(NotIn
)
followForks
- マッチしたPIDに対して再帰的にaction(後述)を適用する
isNamespacePID
- namespaceごとに区切られたPIDであるか否か
values
- PIDの数値
matchCapabilities
- valuesで指定した capabilities を対象にactionを実行する
type
- ケーパビリティセットの指定
- ケーパビリティセットについて
execve(2)
した際に継承できるケーパビリティセットEffective実際にカーネルがスレッドの実行権限を判定するのに使うケーパビリティセット引用元: 加藤泰文, Linuxカーネルのケーパビリティ[1] , https://gihyo.jp/admin/serial/01/linux_containers/0042
operator
- valuesに指定したcapabilityがイベントに含まれているか否か
values
- capabilityの種類
matchCapabilityChanges
- describeやコメントアウトの情報は存在したが、このパラメータに関しては用途が不明瞭であった
- https://github.com/cilium/tetragon/blob/f4ff281c720904961a3f92f61198e101812df709/docs/security-observability-with-ebpf/04_chapter/01_cilium_tracing_policy/01_privileged_pod.yaml#L29
- https://github.com/cilium/tetragon/blob/f4ff281c720904961a3f92f61198e101812df709/vendor/github.com/cilium/tetragon/pkg/k8s/apis/cilium.io/client/crds/v1alpha1/cilium.io_tracingpolicies.yaml#L633
- パラメータはmatchCapabilitiesと同様
matchBinaries
operator
- valuesに指定したバイナリがイベントに含まれているか否か
values
- バイナリの絶対パス
matchActions
- action
- 検知したイベントに対して行うactionを指定する
- 設定できるアクションは以下のようなものがある
var actionTypeTable = map[string]uint32{ "post": ActionTypePost, "followfd": ActionTypeFollowFd, "unfollowfd": ActionTypeUnfollowFd, "sigkill": ActionTypeSigKill, "override": ActionTypeOverride, "copyfd": ActionTypeCopyFd, "geturl": ActionTypeGetUrl, "dnslookup": ActionTypeDnsLookup, } 引用元: <https://github.com/cilium/tetragon/blob/main/pkg/selectors/kernel.go#L29>
特権コンテナの起動を防ぐ TracingPolicy の反映
TracingPolicy を Kubernetes に適用し、反映されているかを確認
$ kubectl apply -f deny_privileged_pod_start.yaml
$ kubectl rollout restart -n kube-system ds/tetragon
$ kubectl get tracingpolicies
NAME AGE
deny-privileged-pod-start 10s
特権を利用するコンテナを実際にデプロイしてみる
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-priv
spec:
replicas: 1
selector:
matchLabels:
app: nginx-priv
template:
metadata:
labels:
app: nginx-priv
spec:
containers:
- name: nginx-priv
image: nginx:latest
ports:
- containerPort: 80
imagePullPolicy: IfNotPresent
#privileged(特権)を使用
securityContext:
privileged: true
Tetragon のログをみて正しく動いているか確認
tetra というルールを使って、ログを確認していきます。
Tetragon のログの取得では、tetra という CLI ツールが利用できます。このツールは export-stdout
の標準出力を見やすい形に整形してくれるツールです。
確認してみると Nginx の起動ができずにいそう
$ kubectl logs ds/tetragon -n kube-system -c export-stdout -f | ./tetra getevents -o compact
📬 open default/nginx-priv-6db9787dc9-d5jsx /usr/sbin/nginx /proc/sys/kernel/ngroups_max 🛑 CAP_SYS_ADMIN
💥 exit default/nginx-priv-6db9787dc9-d5jsx /usr/sbin/nginx -g "daemon off;" SIGKILL 🛑 CAP_SYS_ADMIN
📬 open default/nginx-priv-6db9787dc9-d5jsx /usr/sbin/nginx /proc/sys/kernel/ngroups_max 🛑 CAP_SYS_ADMIN
💥 exit default/nginx-priv-6db9787dc9-d5jsx /usr/sbin/nginx -g "daemon off;" SIGKILL 🛑 CAP_SYS_ADMIN
Nginx のログをみてみると、Web Server のプロセスが起動できずにいそうです🎉
$ kubectl logs pods/nginx-priv-6db9787dc9-6crb4
2022/10/21 06:25:34 [alert] 1#1: worker process 28159 exited on signal 9
2022/10/21 06:25:34 [notice] 1#1: start worker process 28161
2022/10/21 06:25:34 [notice] 1#1: signal 29 (SIGIO) received
Tetragon の監視構築
Prometheus と Grafana を組み合わせて Tetragon を監視するダッシュボードを作成しました。
- 補足
- Prometheus とは Prometheusは時系列のデータベースを採用しているPull型のデータモデルを持っていて、Service Discovery(サービスディスカバリ)という機能によって(監視の対象の)ターゲットを自動的に追従してくれます。さらにPromQL(プロムキューエル)という専用のクエリ言語があり、これを使うことによってシンプルかつ柔軟なクエリを発行することができます。 引用元: さくらのナレッジ, 「今日から始めるPrometheusによるシステム監視(1) 〜Prometheusの特徴とアーキテクチャ〜」, https://knowledge.sakura.ad.jp/27501/#Prometheus
- Grafana とは Grafanaを利用するためには元のデータが必要であるため、データを収集するツール(PrometheusやElasticsearch等)と組み合わせて使われます。可視化に特化しているため、他プロダクトが各自で用意しているダッシュボードよりも時系列グラフの可視化自由度が高いという特徴があります。引用元: NRI, 「Grafana とは?」, https://openstandia.jp/oss_info/grafana/
運用監視に利用できそうだと考えたメトリクス
- tetragon_events_total
- tetragon で検知した全てのイベント
- tetragon_errors_total
- tetragon で取得したすべてのエラーイベント
実際に作成した監視ダッシュボード
このダッシュボードについて
正常率を表示している
(sum(tetragon_events_total) - sum(tetragon_errors_total)) / sum(tetragon_events_total)
エラーの原因を取得できる
sum by (type) (tetragon_errors_total)
まとめ
今回のインターンを通して、eBPFについての基礎的な理解を深め、Tetragonの概要の理解ができました。コンテナ内で特定のファイルを開いた場合にプロセスをKillする設定するTracingPolicyのポリシーについて、時間が足りず掘り下げられませんでした。今後は、今回の「特権コンテナの起動を防ぐ」というポリシーはEKS上では動いたがGKE上ではうまく動作しなかったので、原因の究明をしたいと思います。
インターンの感想
後藤
僕は大学院では生物物理学の研究を行っており、プログラミングも趣味でやっている程度です。ですので、今回のインターンで取り組んだ分野は僕にとって未知の世界でした。 最初はわからないことだらけでしたが、同じチームの小川さんやメンターの方たちのサポートのおかげで、少しずつわかることが増えていき、最後にはこのような形でアウトプットとしてまとめることができました。 何を聞いても「???」だった最初の頃と、このブログを書いている今を比べると、この期間を通してとんでもなく成長できたことを実感します。 2週間という短い間でしたが、非常に充実した時間を過ごせました。 ありがとうございました。
小川
CRDの理解から始まり、Tetragonの導入と検証までを体験しました。 Tetragonは今年5月にリリースされたばかりで情報が少なく、試行錯誤しながら理解していくといった形だったので大変でしたが、その分力がついたと感じています。
メンターの方々は親身に相談に乗っていただいたり一緒に手を動かしていただき、大変スムーズにタスクを進めることができました。 他の参加者の方のレベルも高く、知識の共有をする上で刺激になりました。 掘り下げたい情報や試したいこともあり心残りもありますが、非常に成長できた2週間でした。 貴重な機会をいただき、ありがとうございました。
参考資料
- Sysdigブログ, 「オープンソースでランタイムコンテナセキュリティをKubernetes上に実装する (パート 1)」, https://www.scsk.jp/sp/sysdig/blog/falco/kubernetes_1.html
- Sysdigブログ, 「ランタイムセキュリティとFalcoを使い始める」, https://www.scsk.jp/sp/sysdig/blog/container_security/falco_6.html
- ebpf.io, 「eBPF Documentation」, https://ebpf.io/what-is-ebpf/
- wikipedia,「実行時コンパイラ」, https://ja.wikipedia.org/wiki/実行時コンパイラ
- Hidenori, 「eBPF – BCCチュートリアル 編」, https://zenn.dev/hidenori3/articles/186861b2d5220b
- 青山真也, 「Cilium Projectから公開! eBPFを用いてセキュリティの可観測性をもたらすTetragon」, https://gihyo.jp/article/2022/08/kubernetes-cloudnative-topics-01
- publickey1, 「Linuxカーネル内部をフックするeBPFを用いてセキュリティの可観測性を実現する「Tetragon」がオープンソースで公開」, https://www.publickey1.jp/blog/22/linxuebpftetragon.html