[3-shake 秋季インターンブログ] eBPF によるコンテナセキュリティツールの Tetragon を検証してみた

Sreake事業部

2022.11.14

自己紹介

後藤慶也 (ごとうけいや)

東北大学大学院 理学研究科物理学専攻 修士1年の後藤慶也です。 大学院では酵母における解糖系の振動現象について研究しています。 インフラにおけるセキュリティに興味があり、今回のインターンに参加しました。

小川博徒 (おがわひろと)

東京電機大学 工学部第二部情報通信工学科4年の小川博徒です。

アルバイトでKubernetesに触ったことがありましたが、経験が浅く体系的な知識を身につけたいと思いインターンに参加いたしました。

はじめに

はじめまして、スリーシェイクのSreake 事業部インターン生の後藤慶也と小川博徒です。Sreake 事業部は SRE 技術に強みを持つエンジニアによるコンサルテーションサービスを提供する事業部であり、私たちも SRE 技術の調査と研究を行う目的で2022年10月11日 ~ 24日に開催された短期インターンに参加しました。2週間という期間を使って、eBPF によるコンテナランタイムセキュリティツールの Tetragon の技術検証と運用方法の提案を行いました。以下では、その成果をまとめたいと思います。

コンテナセキュリティとeBPF

DockerやKubernetesを使用してシステムを構築するにあたり、安全な運用を行うためにはコンテナに関するセキュリティが必要不可欠です。

まず最初に取り組むべきことは、イメージの脆弱性に対するセキュリティです。イメージのスキャンを行い、事前に脆弱性を発見することで脆弱性のあるコンテナのデプロイを防止することができます。しかし、たとえスキャン済みのコンテナだったとしても、デプロイ後に脆弱性が発覚するということもあり得るため、イメージのスキャンだけでは不十分です。

そこで重要になるのがコンテナランタイムにおけるセキュリティです。コンテナが実行される本番環境はコンテナライフサイクルにおいて最も期間が長く、運用中にいつ問題が発生するかわかりません。そのため継続してコンテナの動作を監視し、異常が生じた際に迅速に対応するシステムが求められます。

こうしたコンテナランタイムにおける異常な振る舞いを検知するために、Linuxカーネルの機能であるeBPFを利用する方法があります。

本記事は、

前半:eBPFの基礎的な解説と簡単なサンプルの実行(後藤) 後半:kubernates環境へのTetragonのデプロイとその検証結果(小川)

という構成になっています。

eBPFとは

https://ebpf.io/static/logo-big-bcc1301f92f547d65907507c088f2c97.png

eBPFはLinuxカーネル内の様々なイベントをトリガとして、プログラムを実行できるLinuxの仕組みです。この機能によってユーザはカーネルのソースコードを変更することなく、安全かつ効率的にカーネルの機能を追加・拡張することができます。

プログラムを書く ~ 実装までの流れ

https://ebpf.io/static/go-ec58640488770cf5e5b4160ae7c04ae0.png

プログラムの書き方

eBPFのコードには、プログラムを実行するトリガとなるイベント(=フック)の指定や、実行されるプログラムの内容を書きます。

eBPFプログラムはバイトコードの形式でLinuxカーネルに読み込まれるので、C言語でコードを書き、バイトコードを作成することが可能です。しかし、bcclibbpfAyaなどのeBPFプログラムの作成を補助するツールを使うことでC++、Python、Lua、Rustでより簡潔にeBPFコードを書くことができます。

バイトコードに変換されたeBPFプログラムは、eBPFシステムコールでLinuxカーネルに読み込まれます。

プログラムの検証

Linuxカーネルに読み込まれたeBPFプログラムは、まずeBPF Verifierによって、安全に実行可能か検証されます。

検証する条件の例としては

  • eBPFプログラムを読み込むプロセスは要求された権限を持っているか
  • プログラムはクラッシュしたり、システムに危害を加えないか
  • プログラムは常に最後まで実行されるか?(無限ループして次の処理が保留されないか)

などがあります。

JITコンパイル

eBPF Velifierによって安全に実行可能だと判断されたeBPFプログラムは、次にJIT(Just-in-Time) Compilerによって汎用的なバイトコードから、CPUやメモリなどデバイス固有の条件に最適化されたコードへと変換されます。この変換によって、eBPFプログラムはカーネルモジュールとして読み込まれたコードと同じくらい効率的に実行することができます。

イベントへの紐づけ

JIT CompilerによってコンパイルされたeBPFプログラムは、最終的にフックとして指定されたイベントに紐づけられます。これにより、そのイベントが発生するとeBPFプログラムに書かれた処理が実行されるようになります。

フックできるイベントとしてあらかじめ定義されているものは、システムコールや関数の開始・終了、カーネルのトレースポイント、ネットワークイベントなどがあります。必要なイベントが定義されていない場合は、Kprobeを用いることでカーネル空間の任意の場所をフックに指定できます。ユーザ空間の任意の場所はUprobeを使用することでフックとして指定できます。

以上が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 の有無の確認及び登録」の処理を行なっておりました

Tetragon で利用できるAPI リソース(Custom Resource)を確認

$ kubectl api-resources
tracingpolicies cilium.io/v1alpha1 false TracingPolicy

※ Tetragon に関係する cilium.io のもののみ抜粋

TracingPolicy (CRD) を確認

kubectl の出力をみやすくしてくれるプラグイン(kubectl-neat)を導入して確認します

➜ 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を参考にしつつ、実際に動かしてみて明らかになったパラメータの役割をまとめます。

tetragon/cilium.io_tracingpolicies.yaml at f4ff281c720904961a3f92f61198e101812df709 · cilium/tetragon

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

  • ケーパビリティセットの指定
    • ケーパビリティセットについて
    プロセス(実際はスレッド)はそれぞれ4種類のケーパビリティセットというものを持っています。ケーパビリティセットは、内部的にはビット列で、ケーパビリティを持っていれば1がセットされます。PermittedEffectiveとInheritableで持つことを許されるケーパビリティセットInheritableexecve(2)した際に継承できるケーパビリティセットEffective実際にカーネルがスレッドの実行権限を判定するのに使うケーパビリティセット引用元: 加藤泰文, Linuxカーネルのケーパビリティ[1] , https://gihyo.jp/admin/serial/01/linux_containers/0042

operator

  • valuesに指定したcapabilityがイベントに含まれているか否か

values

  • capabilityの種類

matchCapabilityChanges

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週間でした。 貴重な機会をいただき、ありがとうございました。

参考資料

ブログ一覧へ戻る

お気軽にお問い合わせください

SREの設計・技術支援から、
SRE運用内で使用する
ツールの導入など、
SRE全般についてご支援しています。

資料請求・お問い合わせ