💂

Tetragon を使って k8s の Observability を強化する

2024/05/07に公開

概要

tetragon について

Tetragon は extended Berkeley Packet Filter (eBPF) を利用して k8s pod runtime のプロセスや kernel の関数の実行に関するイベントを収集・監視するための OSS プロジェクトです。

https://tetragon.io/

https://github.com/cilium/tetragon

tetragon は Isovalent が管理する OSS プロジェクトであり、他のプロダクトには k8s CNI で有名な Cilium や Hubble があります。tetragon は元々 cilium の機能の一部として開発されてきましたが、2022 年に独立した OSS project という位置づけになったそうです。

https://cilium.io/

https://github.com/cilium/hubble

tetragon でできること

tetragon サイトのトップページにある Revolutionize Your Observability and Security に記載されている通り、k8s 環境における Observability とセキュリティの向上を目的としています。

  • Monitor Process Execution
    • pod 内のプロセスに関するイベントを記録し、プロセスの生成 ~ 終了までのライフサイクルを監視する。
  • Runtime Security Policies
    • コンテナのセキュリティポリシーや不要な権限の監視。
  • Real Time Enforcement
    • eBPF を利用した CRD により独自のポリシーを動的にカーネルに反映させる。

Observability の 3 大要素 (log, metrics, trace) では、k8s クラスタの node や pod, pod 間通信などの状況は逐次観測することができますが、pod のコンテナ内でどのようなプロセスの生成や権限の昇格が行われているかについてはほとんどカバーすることができません(logs 等を頑張ればできるかもしれませんが)。そのため、もし pod やコンテナ、コンテナイメージのセキュリティ対応が不十分な場合、コンテナ上で不適切な権限でプロセスが実行されたり、不正アクセスによって悪意あるプログラムを実行されるような状況が発生する可能性があります。tetragon ではそのような操作をイベントとして監視し、eBPF を用いたポリシーを動的に適用することで悪意あるプロセス実行をカーネルのレイヤーから強制的に終了させる等が実現できるようになっています。

この記事でやること

ドキュメントでは Use Cases で tetragon を使ってどのようなことができるか使用例が示されているため、この記事では上記の内容に沿った検証を行って tetragon の動作や記録されるイベントの詳細を確認したいと思います。

また、tetragon では eBPF を使ったポリシーを使用する都合上 Linux カーネルや eBPF の知識が多少必要になってきます。ただ私自身カーネルの知識はほとんどなくこのあたりを突き詰めていくと泥沼にはまりそうなので、必要になった際に最小限触れる程度にしていきます。

インストール

検証に使用する k8s クラスタは control-plane 用のノード x1, worker node x2 で構成します。

$ k get node -o wide
NAME     STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE       KERNEL-VERSION     CONTAINER-RUNTIME
k8s-m1   Ready    control-plane   46h   v1.29.4   10.0.0.30     <none>        Ubuntu 23.04   6.2.0-34-generic   containerd://1.7.16
k8s-w1   Ready    <none>          46h   v1.29.4   10.0.0.31     <none>        Ubuntu 23.04   6.2.0-34-generic   containerd://1.7.16
k8s-w2   Ready    <none>          46h   v1.29.4   10.0.0.32     <none>        Ubuntu 23.04   6.2.0-34-generic   containerd://1.7.16

tetragon は eBPF を使う都合上、前提条件として ノードの kernel version が 4.19 以上である必要があります。

Tetragon needs Linux kernel version 4.19 or greater.

上記の k get nodeKERNEL-VERSION から各ノードの kernel version が確認できます。今回は 6.2.0 なので ok。新し目の OS を使っていればだいたい条件は満たせると思います。


tetragon はインストール手順 https://tetragon.io/docs/installation/kubernetes/ の通り helm でインストールします。

helm repo add cilium https://helm.cilium.io
helm repo update
helm install tetragon cilium/tetragon -n kube-system

helm でのインストールでは operator と tetragon 本体の daemonset が構築されます。

$ k get pod -n kube-system -o wide -l  app.kubernetes.io/instance=tetragon
NAME                                 READY   STATUS    RESTARTS   AGE   IP           NODE     NOMINATED NODE   READINESS GATES
tetragon-9x6sd                       2/2     Running   0          47h   10.0.0.30    k8s-m1   <none>           <none>
tetragon-m8mnc                       2/2     Running   0          47h   10.0.0.32    k8s-w2   <none>           <none>
tetragon-operator-867595557c-2x9z7   1/1     Running   0          47h   10.244.2.2   k8s-w2   <none>           <none>
tetragon-pz5gh                       2/2     Running   0          47h   10.0.0.31    k8s-w1   <none>           <none>

これで tetragon を使う準備ができたので、Use Cases で紹介されている機能を試していきます。

プロセスのライフサイクルの監視

プロセスイベントの監視

https://tetragon.io/docs/use-cases/process-lifecycle/

POD 内のコンテナ上で(PID 1 で起動しているプロセスとは別の)何らかの新しいプロセスが開始して終了する際、Linux 的にはプロセスの開始時に fork → exec, プロセス終了時に exit のシステムコールが実行されています。tetragon ではデフォルトでこれらのシステムコールが呼ばれた際にイベントとして json 形式の記録を収集しており、コンテナ内でプロセスがいつ始まりいつ終わったかというライフサイクルを監視することが可能となっています。
ドキュメントではデモ用のアプリケーションを使ってライフサイクルイベントの確認を行っていますが、ここでは自分で作成した pod に対してイベントを確認してみます。
まずはじめに、シェルスクリプト内で別のシェルスクリプトを実行する検証用の簡単なコンテナイメージを作成します。

Dockerfile
FROM ubuntu

COPY sub.sh /
COPY subsub.sh /
ENTRYPOINT [ "" ]
CMD [ "" ]

シェルスクリプト sub.sh 内では subsub.sh を呼ぶように設定。

sub.sh
#!/bin/bash

echo "Start sub.sh"
sleep 5
/subsub.sh
sleep 5
echo "Finish sub.sh"
subsub.sh
#!/bin/bash

echo "Start subsub.sh"
sleep 5
echo "Finish subsub.sh"

これを registry.centre.com:5000/process-monitor としてビルドし、以下のマニフェストを使って pod を作成します。

process-monitor.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: process-monitor
  namespace: myapp
  labels:
    app: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: process-monitor
        image: registry.centre.com:5000/process-monitor
        command:
          - sleep
          - "3600"

tetragon では各ノード上に配置される tetragon pod がノード上の他の pod で発生するイベントを自動で収集し、tetragon の pod log から確認できるようになっています。これは k logs -n kube-system [tetragon-pod-name] export-stdout で確認できますが、ドキュメントの通り始めは pod 内の tetra CLI を使ってイベントを取得した方が何が起こっているかわかりやすいため、こちらの方法で見てみます。

まず pod 名と pod が配置されている node を確認。pod 名は process-monitor-98f96db4d-nsc59 で node は k8s-w2

$ k get pod -o wide
NAME                              READY   STATUS    RESTARTS   AGE   IP            NODE     NOMINATED NODE   READINESS GATES
process-monitor-98f96db4d-nsc59   1/1     Running   0          18m   10.244.2.15   k8s-w2   <none>           <none>

上記で確認した node 上で起動している tetragon pod 名を確認。下記の例では tetragon-m8mnc

$ k get pod -o wide -n kube-system -l app.kubernetes.io/instance=tetragon
NAME                                 READY   STATUS    RESTARTS   AGE   IP           NODE     NOMINATED NODE   READINESS GATES
tetragon-9x6sd                       2/2     Running   0          2d    10.0.0.30    k8s-m1   <none>           <none>
tetragon-m8mnc                       2/2     Running   0          2d    10.0.0.32    k8s-w2   <none>           <none>
tetragon-operator-867595557c-2x9z7   1/1     Running   0          2d    10.244.2.2   k8s-w2   <none>           <none>
tetragon-pz5gh                       2/2     Running   0          2d    10.0.0.31    k8s-w1   <none>           <none>

tetragon pod 内の tetragon コンテナで tetra getevents コマンドを実行。オプションは以下。

  • -o compact: 取得されるイベントを簡易表示する
  • --pods [pod_name]: イベント収集対象の pod 名を指定
terminal1
$ k exec -n kube-system -it tetragon-m8mnc -c tetragon -- tetra getevents -o compact --pods process-monitor-98f96db4d-nsc59

これで pod process-monitor-98f96db4d-nsc59 で発生するイベントを待ち受ける状態になり、イベントが発生した際にターミナル上に出力されます。
別のターミナル (terminal 2 とする) を開き、pod 内で先ほど作成した sub.sh を実行します。

terminal2
$ k exec -it process-monitor-98f96db4d-nsc59 -- /sub.sh
Start sub.sh
Start subsub.sh
Finish subsub.sh
Finish sub.sh

pod 内で sub.sh → subsub.sh が実行されるので、シェルスクリプト内の echo や sleep の実行結果が terminal 2 上に出力されます。一方で terminal 1 上ではこれらのプロセスの作成や終了に関する多数のイベントが出力されます。

terminal1
$ k exec -n kube-system -it tetragon-m8mnc -c tetragon -- tetra getevents -o compact --pods process-monitor-98f96db4d-nsc59
🚀 process myapp/process-monitor-98f96db4d-nsc59 /sub.sh /sub.sh
❓ syscall myapp/process-monitor-98f96db4d-nsc59 /sub.sh commit_creds
🚀 process myapp/process-monitor-98f96db4d-nsc59 /usr/bin/sleep 5
💥 exit    myapp/process-monitor-98f96db4d-nsc59 /usr/bin/sleep 5 0
❓ syscall myapp/process-monitor-98f96db4d-nsc59 /sub.sh commit_creds
🚀 process myapp/process-monitor-98f96db4d-nsc59 /subsub.sh /subsub.sh
❓ syscall myapp/process-monitor-98f96db4d-nsc59 /subsub.sh commit_creds
🚀 process myapp/process-monitor-98f96db4d-nsc59 /usr/bin/sleep 5
💥 exit    myapp/process-monitor-98f96db4d-nsc59 /usr/bin/sleep 5 0
💥 exit    myapp/process-monitor-98f96db4d-nsc59 /subsub.sh /subsub.sh 0
❓ syscall myapp/process-monitor-98f96db4d-nsc59 /sub.sh commit_creds
🚀 process myapp/process-monitor-98f96db4d-nsc59 /usr/bin/sleep 5
💥 exit    myapp/process-monitor-98f96db4d-nsc59 /usr/bin/sleep 5 0
💥 exit    myapp/process-monitor-98f96db4d-nsc59 /sub.sh /sub.sh 0

各列の内容は以下のようになっています。

  • 1 列目: イベントの種類
    • process がプロセスの作成、exit がプロセスの終了、syscall が内部的なシステムコールに対応。
  • 2 列目: イベント発生元の pod 名。
    • 今回はイベント収集対象を --pods process-monitor-98f96db4d-nsc59 で指定しているの でこれのみ。
  • 3 列目以降: 実行プロセス、実行時引数やシステムコールの内容

例えば 3, 4 行目は sub.sh 内の sleep 5 に対応していますが、これは sleep コマンドを実行するために内部的にプロセスが exec で実行されたのち exit で終了されるというような具合になっています。

イベント詳細の確認

上記の出力結果は -o compact を指定した際の簡易表記であるため、今度は各イベントの詳細について見ていきます。一度コマンドを中断し、-o compact を外して同じコマンドを実行してイベントを待ち受けます。terminal 2 にて再度 sub.sh を実行すると、今度は上記のイベントの詳細が json 形式で出力されます。

terminal1
$ k exec -n kube-system -it tetragon-m8mnc -c tetragon -- tetra getevents --pods process-monitor-98f96db4d-nsc59
{"process_exec":{"process":{"exec_id":"azhzLXcyOjE3ODgwOTg4MTU5MDQ2NToxMjE1NzQ5","pid":1215749,"uid":0,"cwd":"/","binary":"/sub.sh","arguments":"/sub.sh","flags":"execve rootcwd clone","start_time":"2024-05-02T16:11:28.000705650Z","auid":4294967295,"pod":{"namespace":"myapp","name":"process-monitor-98f96db4d-nsc59","container":{"id":"containerd://59ae3c4ce0ac30ff87ec7037020f767bfcf35b4f84c8f4d7172fb4f75a7ab89f","name":"process-monitor","image":{"id":"registry.centre.com:5000/process-monitor@sha256:dd77f475ef3cf922bad4e33d6795ecd1d4dfa2ad6eb0700fa8d79ea620385b1e","name":"registry.centre.com:5000/process-monitor:latest"},"start_time":"2024-05-02T15:36:40Z","pid":47},"pod_labels":{"app":"myapp","pod-template-hash":"98f96db4d"},"workload":"process-monitor","workload_kind":"Deployment"},"docker":"59ae3c4ce0ac30ff87ec7037020f767","parent_exec_id":"azhzLXcyOjE3NjcyMjMzMTAxMzAxNDoxMjAxMjg1","tid":1215749},"parent":{"exec_id":"azhzLXcyOjE3NjcyMjMzMTAxMzAxNDoxMjAxMjg1","pid":1201285,"uid":0,"cwd":"/run/containerd/io.containerd.runtime.v2.task/k8s.io/1f90009cb0f20435e42a4ec951b6ae592ea2af1065b65d87d85eba375af18d61","binary":"/usr/local/bin/containerd-shim-runc-v2","arguments":"-namespace k8s.io -id 1f90009cb0f20435e42a4ec951b6ae592ea2af1065b65d87d85eba375af18d61 -address /run/containerd/containerd.sock","flags":"execve clone","start_time":"2024-05-02T15:36:40.450126392Z","auid":4294967295,"parent_exec_id":"azhzLXcyOjE3NjcyMjMyNTYzNTY2MjoxMjAxMjc4","tid":1201285}},"node_name":"k8s-w2","time":"2024-05-02T16:11:28.000704070Z"}
...

まずは process myapp/process-monitor-98f96db4d-nsc59 /sub.sh /sub.sh に対応するイベントの詳細を見てみます。イベント詳細はフィールド数が多くて見づらいので、可読性のため適宜 json → yaml 形式に変換して確認します。

process_exec:
  process:
    exec_id: azhzLXcyOjE3ODgwOTg4MTU5MDQ2NToxMjE1NzQ5
    pid: 1215749
    uid: 0
    cwd: /
    binary: /sub.sh
    arguments: /sub.sh
    flags: execve rootcwd clone
    start_time: "2024-05-02T16:11:28.000705650Z"
    auid: 4294967295
    pod:
      namespace: myapp
      name: process-monitor-98f96db4d-nsc59
      container:
        id: containerd://59ae3c4ce0ac30ff87ec7037020f767bfcf35b4f84c8f4d7172fb4f75a7ab89f
        name: process-monitor
        image:
          id: registry.centre.com:5000/process-monitor@sha256:dd77f475ef3cf922bad4e33d6795ecd1d4dfa2ad6eb0700fa8d79ea620385b1e
          name: registry.centre.com:5000/process-monitor:latest
        start_time: "2024-05-02T15:36:40Z"
        pid: 47
      pod_labels:
        app: myapp
        pod-template-hash: 98f96db4d
      workload: process-monitor
      workload_kind: Deployment
    docker: 59ae3c4ce0ac30ff87ec7037020f767
    parent_exec_id: azhzLXcyOjE3NjcyMjMzMTAxMzAxNDoxMjAxMjg1
    tid: 1215749
  parent:
    exec_id: azhzLXcyOjE3NjcyMjMzMTAxMzAxNDoxMjAxMjg1
    pid: 1201285
    uid: 0
    cwd: /run/containerd/io.containerd.runtime.v2.task/k8s.io/1f90009cb0f20435e42a4ec951b6ae592ea2af1065b65d87d85eba375af18d61
    binary: /usr/local/bin/containerd-shim-runc-v2
    arguments: -namespace k8s.io -id 1f90009cb0f20435e42a4ec951b6ae592ea2af1065b65d87d85eba375af18d61 -address /run/containerd/containerd.sock
    flags: execve clone
    start_time: "2024-05-02T15:36:40.450126392Z"
    auid: 4294967295
    parent_exec_id: azhzLXcyOjE3NjcyMjMyNTYzNTY2MjoxMjAxMjc4
    tid: 1201285
node_name: k8s-w2
time: "2024-05-02T16:11:28.000704070Z"

イベント内の各フィールドにはそのイベントが発生した node や pod、コマンドの詳細などが記載されています。重要そうなものについていくつか見てみます。

process_exec

トップレベルの process_exec はこれが process が生成された際(process の exec)に対応するイベントであることを示しています。

parent

parent フィールドにはプロセスの親プロセスの情報等が記載されます。上記のイベントはターミナルから直接 pod 内の sub.sh を実行した際の記録ですが、この場合はコンテナのプロセス、つまり pod が起動している node 上のコンテナに対応するプロセスに関する情報が記載されています。例えば今回の k8s クラスタではコンテナランタイムに containerd を使用しているため、このコンテナに対応するプロセスは binary: /usr/local/bin/containerd-shim-runc-v2 となっています。また、cwd や arguments に含まれる 1f90009cb0f2... は node 上で実際に起動しているコンテナの id に対応しており、対象ノード上でプロセスや id でコンテナを検索することで確認できます。

k8s-w2 ノード
$ ps aux | grep -v grep | grep 1f90009cb0f20435e42a4ec951b6ae592ea2af1065b65d87d85eba375af18d61
root     1201285  0.0  0.1 1238452 14328 ?       Sl   15:36   0:00 /usr/local/bin/containerd-shim-runc-v2 -namespace k8s.io -id 1f90009cb0f20435e42a4ec951b6ae592ea2af1065b65d87d85eba375af18d61 -address /run/containerd/containerd.sock

$ sudo nerdctl -n k8s.io ps -f id=1f90009cb0f2
CONTAINER ID    IMAGE                        COMMAND     CREATED           STATUS    PORTS    NAMES
1f90009cb0f2    registry.k8s.io/pause:3.6    "/pause"    58 minutes ago    Up                 k8s://myapp/process-monitor-98f96db4d-nsc59

process

process フィールドには対象のプロセスに関する詳細が記載されます。重要そうなものは以下。

  • process.binary では実行プロセス(ファイルシステム上のパス)、process.arguments ではそのプロセスに渡された引数が記載されます。このイベントは terminal 2 から pod 内の /sub.sh を実行したという内容なのでそれぞれ /sub.sh が入っています。
  • process.uid はプロセスを実行した pod 内の ユーザー id が記載されます。今回は特にユーザーを指定していないので、process-test のベースイメージ ubuntu のデフォルトユーザー root の uid 0 が入っています。
  • process.flagsgRPC API Reference の flags に記載のとおり対応するシステムコールに関する情報が記載されます。ただあくまでデバッグ目的で記載されるだけであり、信頼できる情報であるとみなすべきではないとのこと。
  • process.pod ではプロセスが実行された pod やコンテナに関する情報が記載されます。

その他のフィールドは Process に説明が記載されています、

node_name

プロセスが実行された pod が存在するノード名が記載されます。


上記の通りイベントの詳細を見ることで実行されたプロセスや pod, node に関する多くの情報を引き出せるようになっています。

他のイベントの詳細についても見てみます。たとえば sub.sh 内で sleep を実行した際のイベントは以下のようになっています。

process_exec:
  process:
    exec_id: azhzLXcyOjE3ODgwOTg4Mzg2NTQ4OToxMjE1NzU1
    pid: 1215755
    uid: 0
    cwd: /
    binary: /usr/bin/sleep
    arguments: "5"
    flags: execve rootcwd clone
    start_time: "2024-05-02T16:11:28.002979631Z"
    auid: 4294967295
    ...
  parent:
    exec_id: azhzLXcyOjE3ODgwOTg4MTU5MDQ2NToxMjE1NzQ5
    pid: 1215749
    uid: 0
    cwd: /
    binary: /sub.sh
    arguments: /sub.sh
    flags: execve rootcwd clone
    start_time: "2024-05-02T16:11:28.000705650Z"
    auid: 4294967295

始めのイベントとの違いは parent.binary が /sub.sh になっていることです。sleep は /sub.sh プロセス (pid: 1215749) から実行される子プロセスという関係であるため、このイベントでは parent が /sub.sh に設定されているということになります。
その他、subsub.sh 内で実行される sleep に対応するイベントは以下。

process_exec:
  process:
    exec_id: azhzLXcyOjE3ODgxNDg5MTYzNDgyMzoxMjE1ODA0
    pid: 1215804
    uid: 0
    cwd: /
    binary: /usr/bin/sleep
    arguments: "5"
    flags: execve rootcwd clone
    start_time: "2024-05-02T16:11:33.010748488Z"
    auid: 4294967295
    ...
  parent:
    exec_id: azhzLXcyOjE3ODgxNDg4NzM3ODQyODoxMjE1ODAz
    pid: 1215803
    uid: 0
    cwd: /
    binary: /subsub.sh
    arguments: /subsub.sh
    flags: execve rootcwd clone
    start_time: "2024-05-02T16:11:33.006492464Z"

この sleep は /subsub.sh 内で実行される子プロセスであるため、parent.binary は /subsub.sh に設定されています。というわけでこの sleep からプロセスを辿ると以下の親子関係であることがわかります。

/usr/local/bin/containerd-shim-runc-v2
└── /sub.sh
    └── /subsub.sh
        └── /usr/bin/sleep

このようにプロセスのイベントでは親プロセスの情報が記載されるため、あるプロセスから別のプロセスが実行されるような状況においてプロセスの親子関係を辿ることができます。ドキュメントではプロセス実行の監視を Observe the complete lifecycle of every process on your machine with Kubernetes context awareness と記載していますが、この表現の通りプロセス生成 ~ 終了までの詳細をプロセス間の関係を含めて追うことができるようになっています。

ファイルへのアクセスの監視

tetragon ではファイルへの read/write や様々な操作時に発生するイベントも補足できます。この機能はデフォルトで有効化されていないため、tetragon の カスタムリソースである TracingPolicy を作成して対象のファイルや操作を指定します。

ファイルへの read/write イベントを記録する

https://tetragon.io/docs/use-cases/filename-access/

ドキュメントでは以下の TracingPolicy を作成することで、pod 内の /etc以下のファイルに read/write アクセスが発生した際のイベントを取得できると記載されています。

https://github.com/cilium/tetragon/blob/main/examples/tracingpolicy/filename_monitoring.yaml

TracingPolicy の内容を深堀りすると長くなるので一旦置いといて、ここでは上記の内容を参考に、指定したパスにあるファイルへのアクセスが発生した際にイベントが取得できるか検証してみます。

検証用のイメージ作成

動作を確認するために、flask を使って /read1, /read2 にアクセスしたら test1.txt, test2.txt のファイルを読み取って返すような python スクリプトを作成します。

main.py
from flask import Flask, request

app = Flask(__name__)


@app.route("/", methods=["GET", "POST"])
def route():
    print(request.get_data())
    return "ok"


@app.route("/read1")
def read():
    with open("/test1.txt", "r") as f:
        msg = f.read()
    return msg

@app.route("/read2")
def read2():
    with open("/test2.txt", "r") as f:
        msg = f.read()
    return msg

if __name__ == "__main__":
    app.run(host="0.0.0.0")

対象ファイルは Dockerfile 内で作成します。

Dockerfile
FROM python:3.12-slim

RUN pip install flask
RUN echo "this is text1" > /test1.txt && \
    echo "this is text2" > /test2.txt

COPY . /
ENTRYPOINT [ "python" ]
CMD [ "/main.py" ]

これを使う pod を作成するマニフェスト

myapp.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: myapp
  labels:
    app: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: registry.centre.com:5000/myapp
        ports:
        - containerPort: 5000

---
apiVersion: v1
kind: Service
metadata:
  name: myapp
  namespace: myapp
  labels:
    run: myapp
spec:
  ports:
  - port: 5000
    protocol: TCP
  selector:
    app: myapp

検証用の TracingPolicy 作成

TracingPolicy に関しては、ドキュメントのポリシーを参考に /test1.txt への read アクセスが発生した際のイベントをキャッチするように記述します。比較のため /test2.txt に対するポリシーは設定しません。

policy.yml
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: myapp-monitor
spec:
  kprobes:
  - call: "security_file_permission"
    syscall: false
    return: true
    args:
    - index: 0
      type: "file" # (struct file *) used for getting the path
    - index: 1
      type: "int" # 0x04 is MAY_READ, 0x02 is MAY_WRITE
    returnArg:
      index: 0
      type: "int"
    returnArgAction: "Post"
    selectors:
    - matchArgs:
      - index: 0
        operator: "Equal"
        values:
        - "/test1.txt"           # Reads to sensitive directories
      - index: 1
        operator: "Equal"
        values:
        - "4" # MAY_READ

動作確認

ライフサイクルのときと同様に対象の pod に対してイベントが発生するのを待ち受けます。

terminal1
$ k exec -it -n kube-system daemonsets/tetragon -c tetragon -- tetra getevents -o compact --pods myapp-c67fd8d6c-p87s6

別のターミナルで [svc_ip]:5000/[path] にリクエストを送信すると、それぞれファイルの内容を読み取ったレスポンスが返ってきます。

terminal2
$ curl  10.103.222.137:5000/read1
this is text1

$ curl  10.103.222.137:5000/read2
this is text2

terminal 1 では python スクリプト (main.py) の中で text1.txt の内容を読み取った時のイベントが記録されます。

terminal1
$ k exec -it -n kube-system daemonsets/tetragon -c tetragon -- tetra getevents -o compact --pods myapp-c67fd8d6c-p87s6
📚 read    myapp/myapp-c67fd8d6c-p87s6 /usr/local/bin/python /test1.txt
📚 read    myapp/myapp-c67fd8d6c-p87s6 /usr/local/bin/python /test1.txt
📚 read    myapp/myapp-c67fd8d6c-p87s6 /usr/local/bin/python /test1.txt
📚 read    myapp/myapp-c67fd8d6c-p87s6 /usr/local/bin/python /test1.txt

イベント詳細を見ると、出力で確認したように 4 つのイベントが記録されています。

{"process_kprobe":{"process":{"exec_id":"azhzLXcyOjI2OTEwMjI5NjA4Mjc5ODoxODQwNTA3","pid":1840507,"uid":0,"cwd":"/","binary":"/usr/local/bin/python","arguments":"/main.py","flags":"execve rootcwd clone","start_time":"2024-04-30T10:43:44.471018318Z","auid":4294967295,"pod":{"namespace":"myapp","name":"myapp-c67fd8d6c-p87s6","container":{"id":"containerd://7506af60826e05f1d0a46ba0c76ac26b5f69cd8e4fbe213887d2fde278e1e7f9","name":"myapp","image":{"id":"registry.centre.com:5000/myapp@sha256:067e34df0cebadd38961dd91ef6a913d3b26528f72ab5c26076e229debdb3421","name":"registry.centre.com:5000/myapp:latest"},"start_time":"2024-04-30T10:43:44Z","pid":1},"pod_labels":{"app":"myapp","pod-template-hash":"c67fd8d6c"},"workload":"myapp","workload_kind":"Deployment"},"docker":"7506af60826e05f1d0a46ba0c76ac26","parent_exec_id":"azhzLXcyOjI2OTEwMTkwNzU4ODU4NjoxODQwNDU1","refcnt":1,"tid":1847109},"parent":{"exec_id":"azhzLXcyOjI2OTEwMTkwNzU4ODU4NjoxODQwNDU1","pid":1840455,"uid":0,"cwd":"/run/containerd/io.containerd.runtime.v2.task/k8s.io/6326d54e72528ec59d5b1d73723b7cbe192749146106aeac4274891ede4db19b","binary":"/usr/local/bin/containerd-shim-runc-v2","arguments":"-namespace k8s.io -id 6326d54e72528ec59d5b1d73723b7cbe192749146106aeac4274891ede4db19b -address /run/containerd/containerd.sock","flags":"execve clone","start_time":"2024-04-30T10:43:44.082523614Z","auid":4294967295,"parent_exec_id":"azhzLXcyOjI2OTEwMTg5NzM5NDExMjoxODQwNDQ3","tid":1840455},"function_name":"security_file_permission","args":[{"file_arg":{"path":"/test1.txt"}},{"int_arg":4}],"return":{"int_arg":0},"action":"KPROBE_ACTION_POST","policy_name":"myapp-monitor"},"node_name":"k8s-w2","time":"2024-04-30T10:48:04.302583244Z"}
{"process_kprobe":{"process":{"exec_id":"azhzLXcyOjI2OTEwMjI5NjA4Mjc5ODoxODQwNTA3","pid":1840507,"uid":0,"cwd":"/","binary":"/usr/local/bin/python","arguments":"/main.py","flags":"execve rootcwd clone","start_time":"2024-04-30T10:43:44.471018318Z","auid":4294967295,"pod":{"namespace":"myapp","name":"myapp-c67fd8d6c-p87s6","container":{"id":"containerd://7506af60826e05f1d0a46ba0c76ac26b5f69cd8e4fbe213887d2fde278e1e7f9","name":"myapp","image":{"id":"registry.centre.com:5000/myapp@sha256:067e34df0cebadd38961dd91ef6a913d3b26528f72ab5c26076e229debdb3421","name":"registry.centre.com:5000/myapp:latest"},"start_time":"2024-04-30T10:43:44Z","pid":1},"pod_labels":{"app":"myapp","pod-template-hash":"c67fd8d6c"},"workload":"myapp","workload_kind":"Deployment"},"docker":"7506af60826e05f1d0a46ba0c76ac26","parent_exec_id":"azhzLXcyOjI2OTEwMTkwNzU4ODU4NjoxODQwNDU1","refcnt":1,"tid":1847109},"parent":{"exec_id":"azhzLXcyOjI2OTEwMTkwNzU4ODU4NjoxODQwNDU1","pid":1840455,"uid":0,"cwd":"/run/containerd/io.containerd.runtime.v2.task/k8s.io/6326d54e72528ec59d5b1d73723b7cbe192749146106aeac4274891ede4db19b","binary":"/usr/local/bin/containerd-shim-runc-v2","arguments":"-namespace k8s.io -id 6326d54e72528ec59d5b1d73723b7cbe192749146106aeac4274891ede4db19b -address /run/containerd/containerd.sock","flags":"execve clone","start_time":"2024-04-30T10:43:44.082523614Z","auid":4294967295,"parent_exec_id":"azhzLXcyOjI2OTEwMTg5NzM5NDExMjoxODQwNDQ3","tid":1840455},"function_name":"security_file_permission","args":[{"file_arg":{"path":"/test1.txt"}},{"int_arg":4}],"return":{"int_arg":0},"action":"KPROBE_ACTION_POST","policy_name":"myapp-monitor"},"node_name":"k8s-w2","time":"2024-04-30T10:48:04.302621628Z"}
{"process_kprobe":{"process":{"exec_id":"azhzLXcyOjI2OTEwMjI5NjA4Mjc5ODoxODQwNTA3","pid":1840507,"uid":0,"cwd":"/","binary":"/usr/local/bin/python","arguments":"/main.py","flags":"execve rootcwd clone","start_time":"2024-04-30T10:43:44.471018318Z","auid":4294967295,"pod":{"namespace":"myapp","name":"myapp-c67fd8d6c-p87s6","container":{"id":"containerd://7506af60826e05f1d0a46ba0c76ac26b5f69cd8e4fbe213887d2fde278e1e7f9","name":"myapp","image":{"id":"registry.centre.com:5000/myapp@sha256:067e34df0cebadd38961dd91ef6a913d3b26528f72ab5c26076e229debdb3421","name":"registry.centre.com:5000/myapp:latest"},"start_time":"2024-04-30T10:43:44Z","pid":1},"pod_labels":{"app":"myapp","pod-template-hash":"c67fd8d6c"},"workload":"myapp","workload_kind":"Deployment"},"docker":"7506af60826e05f1d0a46ba0c76ac26","parent_exec_id":"azhzLXcyOjI2OTEwMTkwNzU4ODU4NjoxODQwNDU1","refcnt":1,"tid":1847109},"parent":{"exec_id":"azhzLXcyOjI2OTEwMTkwNzU4ODU4NjoxODQwNDU1","pid":1840455,"uid":0,"cwd":"/run/containerd/io.containerd.runtime.v2.task/k8s.io/6326d54e72528ec59d5b1d73723b7cbe192749146106aeac4274891ede4db19b","binary":"/usr/local/bin/containerd-shim-runc-v2","arguments":"-namespace k8s.io -id 6326d54e72528ec59d5b1d73723b7cbe192749146106aeac4274891ede4db19b -address /run/containerd/containerd.sock","flags":"execve clone","start_time":"2024-04-30T10:43:44.082523614Z","auid":4294967295,"parent_exec_id":"azhzLXcyOjI2OTEwMTg5NzM5NDExMjoxODQwNDQ3","tid":1840455},"function_name":"security_file_permission","args":[{"file_arg":{"path":"/test1.txt"}},{"int_arg":4}],"return":{"int_arg":0},"action":"KPROBE_ACTION_POST","policy_name":"myapp-monitor"},"node_name":"k8s-w2","time":"2024-04-30T10:48:04.302634756Z"}
{"process_kprobe":{"process":{"exec_id":"azhzLXcyOjI2OTEwMjI5NjA4Mjc5ODoxODQwNTA3","pid":1840507,"uid":0,"cwd":"/","binary":"/usr/local/bin/python","arguments":"/main.py","flags":"execve rootcwd clone","start_time":"2024-04-30T10:43:44.471018318Z","auid":4294967295,"pod":{"namespace":"myapp","name":"myapp-c67fd8d6c-p87s6","container":{"id":"containerd://7506af60826e05f1d0a46ba0c76ac26b5f69cd8e4fbe213887d2fde278e1e7f9","name":"myapp","image":{"id":"registry.centre.com:5000/myapp@sha256:067e34df0cebadd38961dd91ef6a913d3b26528f72ab5c26076e229debdb3421","name":"registry.centre.com:5000/myapp:latest"},"start_time":"2024-04-30T10:43:44Z","pid":1},"pod_labels":{"app":"myapp","pod-template-hash":"c67fd8d6c"},"workload":"myapp","workload_kind":"Deployment"},"docker":"7506af60826e05f1d0a46ba0c76ac26","parent_exec_id":"azhzLXcyOjI2OTEwMTkwNzU4ODU4NjoxODQwNDU1","refcnt":1,"tid":1847109},"parent":{"exec_id":"azhzLXcyOjI2OTEwMTkwNzU4ODU4NjoxODQwNDU1","pid":1840455,"uid":0,"cwd":"/run/containerd/io.containerd.runtime.v2.task/k8s.io/6326d54e72528ec59d5b1d73723b7cbe192749146106aeac4274891ede4db19b","binary":"/usr/local/bin/containerd-shim-runc-v2","arguments":"-namespace k8s.io -id 6326d54e72528ec59d5b1d73723b7cbe192749146106aeac4274891ede4db19b -address /run/containerd/containerd.sock","flags":"execve clone","start_time":"2024-04-30T10:43:44.082523614Z","auid":4294967295,"parent_exec_id":"azhzLXcyOjI2OTEwMTg5NzM5NDExMjoxODQwNDQ3","tid":1840455},"function_name":"security_file_permission","args":[{"file_arg":{"path":"/test1.txt"}},{"int_arg":4}],"return":{"int_arg":0},"action":"KPROBE_ACTION_POST","policy_name":"myapp-monitor"},"node_name":"k8s-w2","time":"2024-04-30T10:48:04.302644682Z"}

一番上のイベントについて見てみます。

process_kprobe:
  process:
    exec_id: azhzLXcyOjI2OTEwMjI5NjA4Mjc5ODoxODQwNTA3
    pid: 1840507
    uid: 0
    cwd: /
    binary: /usr/local/bin/python
    arguments: /main.py
    flags: execve rootcwd clone
    start_time: "2024-04-30T10:43:44.471018318Z"
    auid: 4294967295
    pod:
      namespace: myapp
      name: myapp-c67fd8d6c-p87s6
      container:
        id: containerd://7506af60826e05f1d0a46ba0c76ac26b5f69cd8e4fbe213887d2fde278e1e7f9
        name: myapp
        image:
          id: registry.centre.com:5000/myapp@sha256:067e34df0cebadd38961dd91ef6a913d3b26528f72ab5c26076e229debdb3421
          name: registry.centre.com:5000/myapp:latest
        start_time: "2024-04-30T10:43:44Z"
        pid: 1
      pod_labels:
        app: myapp
        pod-template-hash: c67fd8d6c
      workload: myapp
      workload_kind: Deployment
    docker: 7506af60826e05f1d0a46ba0c76ac26
    parent_exec_id: azhzLXcyOjI2OTEwMTkwNzU4ODU4NjoxODQwNDU1
    refcnt: 1
    tid: 1847109
  parent:
    exec_id: azhzLXcyOjI2OTEwMTkwNzU4ODU4NjoxODQwNDU1
    pid: 1840455
    uid: 0
    cwd: /run/containerd/io.containerd.runtime.v2.task/k8s.io/6326d54e72528ec59d5b1d73723b7cbe192749146106aeac4274891ede4db19b
    binary: /usr/local/bin/containerd-shim-runc-v2
    arguments: -namespace k8s.io -id 6326d54e72528ec59d5b1d73723b7cbe192749146106aeac4274891ede4db19b -address /run/containerd/containerd.sock
    flags: execve clone
    start_time: "2024-04-30T10:43:44.082523614Z"
    auid: 4294967295
    parent_exec_id: azhzLXcyOjI2OTEwMTg5NzM5NDExMjoxODQwNDQ3
    tid: 1840455
  function_name: security_file_permission
  args:
    - file_arg:
        path: /test1.txt
    - int_arg: 4
  return:
    int_arg: 0
  action: KPROBE_ACTION_POST
  policy_name: myapp-monitor
node_name: k8s-w2
time: "2024-04-30T10:48:04.302583244Z"

フィールドの中身について

  • トップレベルは process_exec ではなく process_kprobe になっており、プロセスの生成ではなく TracingPolicy による kprobe の hook により発生したイベントとなっています。
  • process.binary: /usr/local/bin/python, arguments: /main.py により、/main.py 内で対象ファイルの読み取りに関する操作が発生していると判別できます。
  • function_name: security_file_permission は TracingPolicy で指定した kprobes の関数名になっています。
  • action は hook 時の Action が指定されます。TracingPolicy で何も指定しない場合はデフォルトで KPROBE_ACTION_POST が指定されるようです(後述)。

今回 TracingPolicy で指定した security_file_permission はカーネルの関数で実態は https://elixir.bootlin.com/linux/latest/source/security/security.c#L2672 で定義されてます。

/**
 * security_file_permission() - Check file permissions
 * @file: file
 * @mask: requested permissions
 *
 * Check file permissions before accessing an open file.  This hook is called
 * by various operations that read or write files.  A security module can use
 * this hook to perform additional checking on these operations, e.g. to
 * revalidate permissions on use to support privilege bracketing or policy
 * changes.  Notice that this hook is used when the actual read/write
 * operations are performed, whereas the inode_security_ops hook is called when
 * a file is opened (as well as many other operations).  Although this hook can
 * be used to revalidate permissions for various system call operations that
 * read or write files, it does not address the revalidation of permissions for
 * memory-mapped files.  Security modules must handle this separately if they
 * need such revalidation.
 *
 * Return: Returns 0 if permission is granted.
 */
int security_file_permission(struct file *file, int mask)
{
	return call_int_hook(file_permission, 0, file, mask);
}

上記より関数の実行時に file オブジェクトと mask に対応する int を引数に取ることがわかります。そのため、TracingPolicy 内で定義されている args の値はそれぞれこれに対応していることがわかります。

policy.yml
spec:
  kprobes:
  - call: "security_file_permission"
    syscall: false
    args:
    - index: 0
      type: "file" # (struct file *) used for getting the path
    - index: 1
      type: "int" # 0x04 is MAY_READ, 0x02 is MAY_WRITE

実際のイベント記録では file に read が行われた test1.txt の絶対パスが指定されており、int は 4 が渡されています。

args:
    - file_arg:
        path: /test1.txt
    - int_arg: 4

この int は umask に対応しており https://elixir.bootlin.com/linux/latest/source/include/linux/fs.h#L97 あたりで定義されています。4 は MAY_READ なのでファイルへの read 操作となっています。

#define MAY_EXEC		0x00000001
#define MAY_WRITE		0x00000002
#define MAY_READ		0x00000004
#define MAY_APPEND		0x00000008
#define MAY_ACCESS		0x00000010
#define MAY_OPEN		0x00000020
#define MAY_CHDIR		0x00000040
/* called from RCU mode, don't block */
#define MAY_NOT_BLOCK		0x0000008

というわけで、イベント詳細の上記のあたりを見ることで /test1.txt に対して file read 操作が行われたということが確認できます(具体的には file permission で read 権限が付加されているか確認する際のイベントを見ていることになりますが)。

もう一度 TracingPolicy を見返してみると、selectors.matchArgs で file と mask に対して条件を指定しています。これは security_file_permissionが発生するイベントに対して file が /test1.txt, mask が 4 = MAY_READ のときのイベントを記録するという条件になるので、他のファイルに対する操作や /test1.txt への write 操作などはイベントとして補足しないということになります。

policy.yml
    selectors:
    - matchArgs:
      - index: 0
        operator: "Equal"
        values:
        - "/test1.txt"           # Reads to sensitive directories
      - index: 1
        operator: "Equal"
        values:
        - "4" # MAY_READ

そのため、/test1.txt に対する read イベントは terminal 1 に表示されますが、/test2.txt に対するイベントは補足対象外のため実行しても表示されない挙動になっています。

terminal1
$ k exec -it -n kube-system daemonsets/tetragon -c tetragon -- tetra getevents -o compact --pods myapp-c67fd8d6c-p87s6
📚 read    myapp/myapp-c67fd8d6c-p87s6 /usr/local/bin/python /test1.txt
📚 read    myapp/myapp-c67fd8d6c-p87s6 /usr/local/bin/python /test1.txt
📚 read    myapp/myapp-c67fd8d6c-p87s6 /usr/local/bin/python /test1.txt
📚 read    myapp/myapp-c67fd8d6c-p87s6 /usr/local/bin/python /test1.txt

ファイルへの read/write アクセスを拒否する

TracingPolicy では matchActions を追加することで指定した条件を満たすイベントが発生した際にいろいろなアクションを実行することができます。デフォルトでは何も指定しない場合は Post Action が適用されるようになっています。

The Post action allows an event to be transmitted to the agent, from kernelspace to userspace. By default, all TracingPolicy hook will create an event with the Post action except in those situations:

a NoPost action was specified in a matchActions;
a rate-limiting parameter is in place, see details below.
This action allows you to specify parameters for the Post action.

event to be transmitted to the agent, from kernelspace to userspace というのが具体的にどのようなことに対応しているのかはいまいちわかりませんが、今までの検証を見る限り tetragon pod がイベントを補足してログに出力する動作になっていると推測されます。

一方で Enforce File Access Monitoring に記載されている file_monitoring_enforce.yaml の Tracing Policy では、 action: Sigkill を指定することでファイルへの read/write 操作が発生した際にそのプロセスに sigkill を送るようになっています。これにより read/write 操作を行うプロセスを kill することでファイルの読み取りを拒否する例となっています。

https://github.com/cilium/tetragon/blob/main/examples/quickstart/file_monitoring_enforce.yaml

動作確認

上記の例に倣って、/test1.txt に read 操作が発生した際に拒否する動作を確認してみます。
先ほど作成した TracingPolicy に matchActions.actions: Sigkill を追加します。

policy.yml
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: myapp-monitor
spec:
  kprobes:
  - call: "security_file_permission"
    syscall: false
    return: true
    args:
    - index: 0
      type: "file" # (struct file *) used for getting the path
    - index: 1
      type: "int" # 0x04 is MAY_READ, 0x02 is MAY_WRITE
    returnArg:
      index: 0
      type: "int"
    returnArgAction: "Post"
    selectors:
    - matchArgs:
      - index: 0
        operator: "Equal"
        values:
        - "/test1.txt"           # Reads to sensitive directories
      - index: 1
        operator: "Equal"
        values:
        - "4" # MAY_READ
+      matchActions:
+      - action: Sigkill

上記のポリシーを適用し、先ほどと同様に /read1 にアクセスするとレスポンスが帰ってきません。

terminal2
$ curl  10.103.222.137:5000/read1
curl: (52) Empty reply from server

イベントを確認すると exit myapp/myapp-c67fd8d6c-p87s6 /usr/local/bin/python /main.py SIGKILL より /test1.txt の読み取りを行うプロセスが kill されていることがわかります。

terminal1
$ -it -n kube-system daemonsets/tetragon -c tetragon -- tetra getevents -o compact --pods myapp-c67fd8d6c-p87s6
📚 read    myapp/myapp-c67fd8d6c-p87s6 /usr/local/bin/python /test1.txt
📚 read    myapp/myapp-c67fd8d6c-p87s6 /usr/local/bin/python /test1.txt
💥 exit    myapp/myapp-c67fd8d6c-p87s6 /usr/local/bin/python /main.py SIGKILL
🚀 process myapp/myapp-c67fd8d6c-p87s6 /usr/local/bin/python /main.py

また、これによりコンテナ内で起動していた /main.py のプロセスが sigkill されていることがわかります。pod の詳細を確認するとコンテナの再起動により RESTARTS の回数が増えていることが確認できます(このあたりはイメージ内のエラーハンドリングの処理によっても異なるかも)。

$ k get pod
NAME                    READY   STATUS    RESTARTS      AGE
myapp-c67fd8d6c-p87s6   1/1     Running   2 (20s ago)   138m

read 操作は umask 4 でしたが write は 2, exec は 1 なので、TracingPolicy でこれらを指定すれば write 操作も sigkill できると想定されます。これを確認するため、前の python スクリプトを修正してアクセスするパスに応じて test.sh に read, write, exec を実行できるようにします。

main.py
import subprocess

from flask import Flask, request

app = Flask(__name__)


@app.route("/", methods=["GET", "POST"])
def route():
    print(request.get_data())
    return "ok"


@app.route("/test_read")
def test_read():
    with open("/test.sh", "r") as f:
        msg = f.read()
    return msg



@app.route("/test_write")
def test_write():
    added = "#!/bin/sh\necho 'added line'\n"
    with open("/test.sh", "w") as f:
        f.write(added)
    return "write complete"


@app.route("/test_exec")
def test_exec():
    try:
        ret = subprocess.run(
            ["/test.sh"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True
        )
        if ret.returncode == 0:
            return ret.stdout
        else:
            return ret.stderr
    except Exception as e:
        return str(e)


if __name__ == "__main__":
    app.run(host="0.0.0.0")
test.sh
!/bin/sh

sleep 5

echo "complete"

TracingPolicy では write, exec 操作を拒否するために 2, 1 を追加します。

    selectors:
    - matchArgs:
      - index: 0
        operator: "Equal"
        values:
        - "/test.sh"           # Reads to sensitive directories
      - index: 1
        operator: "Equal"
        values:
+        - "2"
+        - "1"

試してみると想定通り read は成功しますが write は拒否されます。

terminal 2
$ curl 10.99.149.89:5000/test_read
#!/bin/sh

sleep 5

echo "complete"

$ curl 10.99.149.89:5000/test_write
curl: (52) Empty reply from server
terminal 1
$ k exec -n kube-system -it tetragon-m8mnc -c tetragon -- tetra getevents -o compact --pods read-write-75dbf6896b-pp8j7
📝 write   myapp/read-write-75dbf6896b-pp8j7 /usr/local/bin/python /test.sh
📝 write   myapp/read-write-75dbf6896b-pp8j7 /usr/local/bin/python /test.sh
💥 exit    myapp/read-write-75dbf6896b-pp8j7 /usr/local/bin/python /main.py SIGKILL
🚀 process myapp/read-write-75dbf6896b-pp8j7 /usr/local/bin/python /main.py

一方で test.sh の exec に関しては TracingPolicy で指定しているにも関わらず特に拒否されずに成功します。

terminal1
$ k exec -n kube-system -it tetragon-m8mnc -c tetragon -- tetra getevents -o compact --pods read-write-75dbf6896b-pp8j7
🚀 process myapp/read-write-75dbf6896b-pp8j7 /test.sh /test.sh
🚀 process myapp/read-write-75dbf6896b-pp8j7 /usr/bin/sleep 5
💥 exit    myapp/read-write-75dbf6896b-pp8j7 /usr/bin/sleep 5 0
💥 exit    myapp/read-write-75dbf6896b-pp8j7 /test.sh /test.sh 0

このことから exec 時には security_file_permission が実行されないようなので別の kernel 関数などを TracingPolicy に設定する必要があります。

binary の実行を拒否する

Advanced Process execution を見てみると binary の実行時は security_bprm_creds_from_file というカーネル関数が実行されるようなので、こちらに hook を仕掛けて確認してみます。
参考: https://elixir.bootlin.com/linux/v6.8.9/source/include/linux/security.h#L298

TracingPolicy は以下。

policy.yml
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: read-write-monitor
spec:
  kprobes:
   - call: "security_bprm_creds_from_file"
     syscall: false
     args:
     - index: 0
       type: "nop"
     - index: 1
       type: "file"

これでイベントを確認してみると、確かに exec 時には security_bprm_creds_from_file が呼ばれていそうです。

terminal1
$ k exec -n kube-system -it tetragon-m8mnc -c tetragon -- tetra getevents -o compact --pods read-write-75dbf6896b-pp8j7
❓ syscall myapp/read-write-75dbf6896b-pp8j7 /usr/local/bin/python security_bprm_creds_from_file
🚀 process myapp/read-write-75dbf6896b-pp8j7 /test.sh /test.sh
❓ syscall myapp/read-write-75dbf6896b-pp8j7 /test.sh security_bprm_creds_from_file
🚀 process myapp/read-write-75dbf6896b-pp8j7 /usr/bin/sleep 5
💥 exit    myapp/read-write-75dbf6896b-pp8j7 /usr/bin/sleep 5 0
💥 exit    myapp/read-write-75dbf6896b-pp8j7 /test.sh /test.sh 0

1 つめの /usr/local/bin/python security_bprm_creds_from_file のイベント詳細は以下のようになっています。

process_kprobe:
  process:
    exec_id: azhzLXcyOjI1OTUwNTUwMDc1NTA2OToxNzU5MjU1
    pid: 1759255
    uid: 0
    cwd: /
    binary: /usr/local/bin/python
    arguments: /main.py
    flags: execve
    start_time: "2024-05-03T14:36:23.619870218Z"
    auid: 4294967295
   ...
  parent:
    exec_id: azhzLXcyOjI1NDAwODgyMDY2MjkwODoxNzIyMjA4
    pid: 1722208
    uid: 0
    cwd: /
    binary: /usr/local/bin/python
    arguments: /main.py
    flags: execve rootcwd clone
    start_time: "2024-05-03T13:04:46.939776203Z"
    auid: 4294967295
    ...
  function_name: security_bprm_creds_from_file
  args:
    - file_arg:
        path: /usr/bin/dash
        permission: -rwxr-xr-x
  action: KPROBE_ACTION_POST
  policy_name: read-write-monitor
  return_action: KPROBE_ACTION_POST
node_name: k8s-w2
time: "2024-05-03T14:36:23.620270828Z"

function_name が hook を仕掛けた関数名 (security_bprm_creds_from_file)、args[].file_arg の中身が実行される binary の詳細になっています。上記では /usr/bin/dash となっていますが、これは

  1. /usr/bin/sh でシェルスクリプト /test.sh を実行しようとする。
  2. Debian 系でのコンテナイメージでは /usr/bin/sh/usr/bin/dash のシンボリックリンクになっている。
root@read-write-75dbf6896b-pp8j7:/# ls -l /usr/bin/sh
lrwxrwxrwx 1 root root 4 Jan  5  2023 /usr/bin/sh -> dash

root@read-write-75dbf6896b-pp8j7:/# ls -l /usr/bin/dash
-rwxr-xr-x 1 root root 125640 Jan  5  2023 /usr/bin/dash
  1. /usr/bin/dash の binary が実行されシェルスクリプトの内容が解釈される。

という処理になっています。

3 行目の syscall myapp/read-write-75dbf6896b-pp8j7 /test.sh security_bprm_creds_from_file のイベント詳細では file_arg.path: /usr/bin/sleep になっていますが、これは /test.sh 内で sleep を実行する処理に対応します。

  function_name: security_bprm_creds_from_file
  args:
    - file_arg:
        path: /usr/bin/sleep
        permission: -rwxr-xr-x

security_bprm_creds_from_file に hook を仕掛けることでイベントを補足できることがわかったので、今度はこれに selector を追加して特定の binary を実行しようとした際に sigkill を送信して拒否する動作を確認してみます。
特定の binary の実行を拒否するには TracingPolicy に selector を追加しますsecurity_bprm_creds_from_file の定義は以下のようになっており、 2 つめの引数 file に実行する binary のパスが指定されます。

int security_bprm_creds_from_file(struct linux_binprm *bprm, const struct file *file);

なので例えば sleep コマンドを対象とする場合は matchArgs にこの条件を指定します。また、matchActions[].action: Sigkill とすることで sleep コマンドが実行された時に sigkill を送信します。これらの条件を合わせて TracingPolicy に以下を追加します。

policy
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: read-write-monitor
spec:
  kprobes:
   - call: "security_bprm_creds_from_file"
     syscall: false
     args:
     - index: 0
       type: "nop"
     - index: 1
       type: "file"
+     selectors:
+      - matchArgs:
+        - index: 1
+          operator: "Equal"
+          values:
+            - "/usr/bin/sleep"
+        matchActions:
+          - action: Sigkill

このポリシーを適用すると、test.sh 内で実行した sleep コマンドが 4 行目の /usr/bin/sleep 5 SIGKILL で kill されていることが確認できます。

terminal 1
$ k exec -n kube-system -it tetragon-m8mnc -c tetragon -- tetra getevents -o compact --pods read-write-75dbf6896b-pp8j7
🚀 process myapp/read-write-75dbf6896b-pp8j7 /test.sh /test.sh
❓ syscall myapp/read-write-75dbf6896b-pp8j7 /test.sh security_bprm_creds_from_file
🚀 process myapp/read-write-75dbf6896b-pp8j7 /usr/bin/sleep 5
💥 exit    myapp/read-write-75dbf6896b-pp8j7 /usr/bin/sleep 5 SIGKILL
💥 exit    myapp/read-write-75dbf6896b-pp8j7 /test.sh /test.sh 0

また、matchArgs を /usr/bin/dash に指定した場合は /test.sh を実行するための dash の起動が kill されるので、この場合は /test.sh がすぐに終了します。

terminal 1
$ k exec -n kube-system -it tetragon-m8mnc -c tetragon -- tetra getevents -o compact --pods read-write-75dbf6896b-pp8j7
❓ syscall myapp/read-write-75dbf6896b-pp8j7 /usr/local/bin/python security_bprm_creds_from_file
🚀 process myapp/read-write-75dbf6896b-pp8j7 /test.sh /test.sh
💥 exit    myapp/read-write-75dbf6896b-pp8j7 /test.sh /test.sh SIGKILL

test.sh のようなシェルスクリプトは単なるテキストファイルなのでこれ自体の実行を制限することはできませんが、シェルスクリプト内で実行される各コマンドやシェルスクリプトを解釈するための shell 自体に sigkill を送信するように設定することで間接的に実行を拒否できます。

このように特定の binary の実行を拒否したい場合は security_bprm_creds_from_file に hook を設定し、selector に適切な条件をつけた TracingPolicy を作成することで実現できます。

Linux process credentials 変更の監視

https://tetragon.io/docs/use-cases/linux-process-credentials/

Linux ではプロセス毎に process credentials と呼ばれる属性のようなものが設定されており、これに応じてファイルへのアクセス可否やセキュリティチェックが実行されます。ドキュメントでは以下の要素が挙げられています。

  • Traditional UNIX credentials
  • Linux Capabilities
  • Secure management flags (securebits)
  • Linux Security Module (LSM)

Linux Capabilities は docker や kubernetes でたまに --cap-add--cap-drop 等で capabilities を設定するような場面がありますが、これは上記の linux capabilities を操作しています。どのような項目があるかは検索すると色々出てきますが、例えば docker ドキュメント を参照。
LSM は AppArmor や SELinux などがありますが、このあたりは k8s でも pod に設定できるため馴染みがあるかも。例えば k8s ドキュメント を参照。

process credentials は sudo でルートユーザーとしてコマンドを実行したり、su で別のユーザーに切り替える際などに変更が伴います。ドキュメントではこのようなイベントを補足する方法として sys_setuid などの システムコールを補足して監視する方法 と commit_creds などの カーネル関数を監視する方法 が紹介されています。カーネル関数を監視する方法では以下のようなメリットがあるようなのでこちらの動作を試してみます。

  • Not vulnerable to user space arguments tampering.
  • Ability to display the full new credentials to be applied.
  • It is more reliable since it has full context on where and how the new credentials should be applied including the user namespace.
  • A catch all layer for all system calls, and every normal kernel path that manipulate credentials.
  • One potential disadvantage is that this approach may generate a lot of events, so appropriate filtering must be applied to reduce the noise.

検証用イメージの作成

検証では Monitor Process Credentials changes at the Kernel layer の内容に基づき、コンテナ内であるユーザから別のユーザに su する際のイベントを確認してみます。このために以下のユーザーを事前に作成したコンテナイメージを用意します。

username uid
user1 1001
user2 1002
Dockerfile
FROM ubuntu

ENV user1=user1
ENV user2=user2

RUN apt update && apt install -y sudo openssl

RUN useradd -d /home/${user1} -s /bin/bash -m -b /home/${user1} --uid 1001 ${user1} -p $(openssl passwd -1 ${user1}) && \
    useradd -d /home/${user2} -s /bin/bash -m -b /home/${user2} --uid 1002 ${user2} -p $(openssl passwd -1 ${user2})

RUN usermod -aG sudo ${user1} && \
    usermod -aG sudo ${user2}

USER user1
ENTRYPOINT [ "" ]
CMD [ "" ]

これをイメージ名 registry.centre.com:5000/process-test として build し、以下のマニフェストで pod を作成します。

process.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: process-test
  namespace: myapp
  labels:
    app: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp
        image: registry.centre.com:5000/process-test
        ports:
        - containerPort: 5000
        command:
          - sleep
          - "3600"

---
apiVersion: v1
kind: Service
metadata:
  name: process-test
  namespace: myapp
  labels:
    run: myapp
spec:
  ports:
  - port: 5000
    protocol: TCP
  selector:
    app: myapp

動作確認

イベントを補足するための TracingPolicy はチュートリアルに記載されているものをそのまま使用します。

apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: "process-creds-changed"
  #annotations:
    #author: "Djalal Harouni"
spec:
  kprobes:
  - call: "commit_creds"
    syscall: false
    args:
    - index: 0  # The new credentials to apply
      type: "cred"
    selectors:
      - matchNamespaces:
        - namespace: Pid
          operator: NotIn
          values:
          - "host_ns"
        matchActions:
          - action: Post
            rateLimit: "1m"

ファイルアクセスの時と同様に 1 つめのターミナルでは tetragon pod 上で tetra getevents を実行してイベントを待ち受けます。

terminal1
k exec -n kube-system -it tetragon-m8mnc -c tetragon -- tetra getevents -o compact --pods process-test-77f84fdf95-stqwd

別のターミナルで検証用 pod にログインし、su user2 で user2 に切り替えます。

terminal2
$ k exec -it process-test-77f84fdf95-stqwd -- bash
user1@process-test-77f84fdf95-stqwd:/$ su user2
Password:
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.

user2@process-test-77f84fdf95-stqwd:/$

このとき 1 つめのターミナルではかなりの量のイベントが発生していることが確認できます。

terminal1
🚀 process myapp/process-test-77f84fdf95-stqwd /usr/bin/su user2
❓ syscall myapp/process-test-77f84fdf95-stqwd /usr/bin/su commit_creds
❓ syscall myapp/process-test-77f84fdf95-stqwd /usr/bin/su commit_creds
🚀 process myapp/process-test-77f84fdf95-stqwd /bin/bash
❓ syscall myapp/process-test-77f84fdf95-stqwd /bin/bash commit_creds
🚀 process myapp/process-test-77f84fdf95-stqwd /usr/bin/groups
💥 exit    myapp/process-test-77f84fdf95-stqwd /usr/bin/groups  0
❓ syscall myapp/process-test-77f84fdf95-stqwd /bin/bash commit_creds
🚀 process myapp/process-test-77f84fdf95-stqwd /usr/bin/cat
💥 exit    myapp/process-test-77f84fdf95-stqwd /usr/bin/cat  0
❓ syscall myapp/process-test-77f84fdf95-stqwd /bin/bash commit_creds
🚀 process myapp/process-test-77f84fdf95-stqwd /usr/bin/dircolors -b
💥 exit    myapp/process-test-77f84fdf95-stqwd /usr/bin/dircolors -b 0

su user2 を実行した際に上記のようなイベントが発生することがわかったので、今度は -o compact を外してイベントの詳細を確認します。

terminal1
k exec -n kube-system -it tetragon-m8mnc -c tetragon -- tetra getevents --pods process-test-77f84fdf95-stqwd

terminal 2 で再度 su user2 を実行すると terminal1 にイベント詳細が大量に出力されます。まずはじめに出力されるイベントの詳細を見てみます。

イベント詳細
process_kprobe:
  process:
    exec_id: azhzLXcyOjk3MjYwNjA2NDkyNjc5OjY2ODA4Nw==
    pid: 668087
    uid: 1001
    cwd: /
    binary: /usr/bin/su
    arguments: user2
    flags: execve
    start_time: "2024-05-01T17:32:18.725608142Z"
    auid: 4294967295
    pod:
      namespace: myapp
      name: process-test-77f84fdf95-stqwd
      container:
        id: containerd://4b670843d77b0983c820b930a92ab7631a9fcb297310052099397312f1045841
        name: myapp
        image:
          id: registry.centre.com:5000/process-test@sha256:ae86b86057f2da2aaea4174c1af956ef00b6e9ee05932cf14750a84e3bad0b8d
          name: registry.centre.com:5000/process-test:latest
        start_time: "2024-05-01T16:55:04Z"
        pid: 16
      pod_labels:
        app: myapp
        pod-template-hash: 77f84fdf95
      workload: process-test
      workload_kind: Deployment
    docker: 4b670843d77b0983c820b930a92ab76
    parent_exec_id: azhzLXcyOjk3MjM5NzM3MDc0MTU5OjY2NzkzOQ==
    refcnt: 1
    tid: 668087
  parent:
    exec_id: azhzLXcyOjk3MjM5NzM3MDc0MTU5OjY2NzkzOQ==
    pid: 667939
    uid: 1001
    cwd: /
    binary: /usr/bin/su
    arguments: user2
    flags: execve rootcwd clone
    start_time: "2024-05-01T17:31:57.856189078Z"
    auid: 4294967295
    pod:
      namespace: myapp
      name: process-test-77f84fdf95-stqwd
      container:
        id: containerd://4b670843d77b0983c820b930a92ab7631a9fcb297310052099397312f1045841
        name: myapp
        image:
          id: registry.centre.com:5000/process-test@sha256:ae86b86057f2da2aaea4174c1af956ef00b6e9ee05932cf14750a84e3bad0b8d
          name: registry.centre.com:5000/process-test:latest
        start_time: "2024-05-01T16:55:04Z"
        pid: 16
      pod_labels:
        app: myapp
        pod-template-hash: 77f84fdf95
      workload: process-test
      workload_kind: Deployment
    docker: 4b670843d77b0983c820b930a92ab76
    parent_exec_id: azhzLXcyOjk3MjI3NTMzMjg4OTk1OjY2NzgzMQ==
    tid: 667939
  function_name: commit_creds
  args:
    - process_credentials_arg:
        uid: 1001
        gid: 1002
        euid: 0
        egid: 1002
        suid: 0
        sgid: 1002
        fsuid: 0
        fsgid: 1002
        caps:
          permitted:
            - CAP_CHOWN
            - DAC_OVERRIDE
            - CAP_FOWNER
            - CAP_FSETID
            - CAP_KILL
            - CAP_SETGID
            - CAP_SETUID
            - CAP_SETPCAP
            - CAP_NET_BIND_SERVICE
            - CAP_NET_RAW
            - CAP_SYS_CHROOT
            - CAP_MKNOD
            - CAP_AUDIT_WRITE
            - CAP_SETFCAP
          effective:
            - CAP_CHOWN
            - DAC_OVERRIDE
            - CAP_FOWNER
            - CAP_FSETID
            - CAP_KILL
            - CAP_SETGID
            - CAP_SETUID
            - CAP_SETPCAP
            - CAP_NET_BIND_SERVICE
            - CAP_NET_RAW
            - CAP_SYS_CHROOT
            - CAP_MKNOD
            - CAP_AUDIT_WRITE
            - CAP_SETFCAP
        user_ns:
          level: 0
          uid: 0
          gid: 0
          ns:
            inum: 4026531837
            is_host: true
  action: KPROBE_ACTION_POST
  policy_name: process-creds-changed
  return_action: KPROBE_ACTION_POST
node_name: k8s-w2
time: "2024-05-01T17:32:18.725874725Z"

このイベント内の process.uid: 1001"binary": "/usr/bin/su", "arguments": "user2" から、uid:1001 の user1 が su user2 を実行した際の記録と判別できます。args[0].process_credentials_arg からは "uid": 1001, "gid": 1002, などが確認できますが、user1 から user2 に process credentials を変更する際の値に対応していることが確認できます。euid や suid が 0 なのはおそらく su 実行時に sudo 相応の権限が必要になるため。
参考: https://book.hacktricks.xyz/v/jp/linux-hardening/privilege-escalation/euid-ruid-suid

フィールドの中身について

commit_creds は https://elixir.bootlin.com/linux/latest/source/kernel/cred.c#L392 で定義されています。

/**
 * commit_creds - Install new credentials upon the current task
 * @new: The credentials to be assigned
 *
 * Install a new set of credentials to the current task, using RCU to replace
 * the old set.  Both the objective and the subjective credentials pointers are
 * updated.  This function may not be called if the subjective credentials are
 * in an overridden state.
 *
 * This function eats the caller's reference to the new credentials.
 *
 * Always returns 0 thus allowing this function to be tail-called at the end
 * of, say, sys_setgid().
 */
int commit_creds(struct cred *new)

commit_creds の引数の struct cred の定義は https://elixir.bootlin.com/linux/latest/source/include/linux/cred.h#L111

    struct cred {
	atomic_long_t	usage;
	kuid_t		uid;		/* real UID of the task */
	kgid_t		gid;		/* real GID of the task */
	kuid_t		suid;		/* saved UID of the task */
	kgid_t		sgid;		/* saved GID of the task */
	kuid_t		euid;		/* effective UID of the task */
	kgid_t		egid;		/* effective GID of the task */
	kuid_t		fsuid;		/* UID for VFS ops */
	kgid_t		fsgid;		/* GID for VFS ops */
	unsigned	securebits;	/* SUID-less security management */
	kernel_cap_t	cap_inheritable; /* caps our children can inherit */
    ...

struct cred では uid や cap_inheritable が定義されていることがわかります。そのため、イベント内の process_credentials_arg フィールド以下に設定されている値は commit_creds を実行する際に引数に渡している値であると推測できます。

その後に出力されているイベントもみていきます。

process_exec:
  process:
    exec_id: azhzLXcyOjk3MjYwNjA3NTU2ODE3OjY2ODA4Nw==
    pid: 668087
    uid: 1002
    cwd: /
    binary: /bin/bash
    flags: execve rootcwd clone
    start_time: "2024-05-01T17:32:18.726671136Z"
    auid: 4294967295
    pod:
     ...
  parent:
    exec_id: azhzLXcyOjk3MjM5NzM3MDc0MTU5OjY2NzkzOQ==
    pid: 667939
    uid: 1001
    cwd: /
    binary: /usr/bin/su
    arguments: user2
    flags: execve rootcwd clone
    start_time: "2024-05-01T17:31:57.856189078Z"
    auid: 4294967295
    pod:
    ...
node_name: k8s-w2
time: "2024-05-01T17:32:18.726670385Z"

このイベントでは parent"binary": "/usr/bin/su", "arguments": "user2",process"uid": 1002, "binary": "/bin/bash" であることから、user1 → user2 に切替時のパスワード認証に成功し、user2 内で interactive shell の /bin/bash を開始した際のイベントに対応していると判断できます (コンパクト表示の際の上から 4 つめの /bin/bash に対応)。

🚀 process myapp/process-test-77f84fdf95-stqwd /usr/bin/su user2
❓ syscall myapp/process-test-77f84fdf95-stqwd /usr/bin/su commit_creds
❓ syscall myapp/process-test-77f84fdf95-stqwd /usr/bin/su commit_creds
🚀 process myapp/process-test-77f84fdf95-stqwd /bin/bash
❓ syscall myapp/process-test-77f84fdf95-stqwd /bin/bash commit_creds

ちなみに上記のイベント直後に process.binary 等が同じ以下のイベントも出力されていますが、こちらは "function_name": "commit_creds" が含まれていることから、上から 5 つめの /bin/bash commit_creds の方のイベントに対応することがわかります。

process_kprobe:
  process:
    exec_id: azhzLXcyOjk3MjYwNjEwMzA2OTIxOjY2ODA4OA==
    pid: 668088
    uid: 1002
    cwd: /
    binary: /bin/bash
    flags: execve
    start_time: "2024-05-01T17:32:18.729421450Z"
    auid: 4294967295
    pod:
    ...
  parent:
    exec_id: azhzLXcyOjk3MjYwNjA3NTU2ODE3OjY2ODA4Nw==
    pid: 668087
    uid: 1002
    cwd: /
    binary: /bin/bash
    flags: execve rootcwd clone
    start_time: "2024-05-01T17:32:18.726671136Z"
    auid: 4294967295
    pod:
    ...
  function_name: commit_creds
  args:
    - process_credentials_arg:
        uid: 1002
        gid: 1002
        euid: 1002
        egid: 1002
        suid: 1002
        sgid: 1002
        fsuid: 1002
        fsgid: 1002
        caps: {}
        user_ns:
          level: 0
          uid: 0
          gid: 0
          ns:
            inum: 4026531837
            is_host: true
  action: KPROBE_ACTION_POST
  policy_name: process-creds-changed
  return_action: KPROBE_ACTION_POST
node_name: k8s-w2
time: "2024-05-01T17:32:18.730196100Z"

このように Linux process credentials の変更を伴うイベントを監視することで、コンテナ内でユーザーを切り替えてプロセスが実行されるような操作を検出することができます。
使い道としてはドキュメントにあるように一般ユーザーから root ユーザーに昇格して意図しないファイルの書き換えや悪意のあるスクリプト実行を検出・拒否することでセキュリティを強化することがあります(このあたりは基本的に pod に security context を適切に設定したり、コンテナイメージで適切なユーザーを作成することで対応できるのでここまでやる必要はあるのかという気もしますが)。

Capabilities について

上記のイベント詳細内に process_credentials_arg.cap.permitted などが設定されているイベントがありましたが、このフィールドにはデフォルトで有効化されている linux capabilities が記載されています。また、k8s では pod マニフェストの定義に securityContext を設定することで capabilities を追加・削除することが可能となっています。ここでは Set capabilities for a Container のように capabilities を追加した際にコンテナ内プロセスにも反映されるのかどうかを見てみます。
先程の process.yml に NET_ADMIN, SYS_TIME の capabilities を追加します。

process.yml
spec:
  template:
    spec:
      containers:
      - name: myapp
        image: registry.centre.com:5000/process-test
        ports:
        - containerPort: 5000
        command:
          - sleep
          - "3600"
+        securityContext:
+          capabilities:
+            add: ["NET_ADMIN", "SYS_TIME"]

先程と同様に pod 内でユーザー切り替えを実行し、その時のイベントを確認します。

  args:
    - process_credentials_arg:
        uid: 1001
        gid: 1002
        euid: 0
        egid: 1002
        suid: 0
        sgid: 1002
        fsuid: 0
        fsgid: 1002
        caps:
          permitted:
            - CAP_CHOWN
            - DAC_OVERRIDE
            - CAP_FOWNER
            - CAP_FSETID
            - CAP_KILL
            - CAP_SETGID
            - CAP_SETUID
            - CAP_SETPCAP
            - CAP_NET_BIND_SERVICE
            - CAP_NET_ADMIN
            - CAP_NET_RAW
            - CAP_SYS_CHROOT
            - CAP_SYS_TIME
            - CAP_MKNOD
            - CAP_AUDIT_WRITE
            - CAP_SETFCAP
          effective:
            - CAP_CHOWN
            - DAC_OVERRIDE
            - CAP_FOWNER
            - CAP_FSETID
            - CAP_KILL
            - CAP_SETGID
            - CAP_SETUID
            - CAP_SETPCAP
            - CAP_NET_BIND_SERVICE
            - CAP_NET_ADMIN
            - CAP_NET_RAW
            - CAP_SYS_CHROOT
            - CAP_SYS_TIME
            - CAP_MKNOD
            - CAP_AUDIT_WRITE
            - CAP_SETFCAP
        user_ns:
          ...

先程の結果と比較すると、cap.permittedcap.effective にそれぞれ CAP_NET_ADMIN, CAP_SYS_TIME が追加されていることがわかります。よって pod に指定した capabilities が実際に追加されていることがイベント記録からも確認できます。

その他

本編の記事と直接関係はないが、調査の中で役に立ちそうな情報を記載。

TracingPolicy 内の引数について

例えば Signal Action の例ではシステムコールとして sys_write が指定されており、args が 3 つ設定されています。

- call: "sys_write"
  syscall: true
  args:
  - index: 0
    type: "fd"
  - index: 1
    type: "char_buf"
    sizeArgIndex: 3
  - index: 2
    type: "size_t"

これが何を示しているかというとシステムコールの関数に渡される引数に対応しています。
https://elixir.bootlin.com/linux/v4.9/source/include/linux/syscalls.h#L567

asmlinkage long sys_write(unsigned int fd, const char __user *buf,
			  size_t count);

定義によると引数が 3 つで type は第一引数から順に fd, char buf, size_t となっているため、それぞれ args の type に対応していることがわかります。なお TracingPolicy の定義は https://github.com/cilium/tetragon/blob/main/pkg/k8s/apis/cilium.io/client/crds/v1alpha1/cilium.io_tracingpolicies.yaml にあり、args で指定可能な type は https://github.com/cilium/tetragon/blob/367c4a004a35089a07d9f05eeef16359ceb1338f/pkg/k8s/apis/cilium.io/client/crds/v1alpha1/cilium.io_tracingpolicies.yaml#L150 に一覧があります。
なので args が何を示しているかは基本的にソースコードの引数を見れば良さそう。

TracingPolicy の記述例

github examples/tracingpolicy にたくさんあるのでこちらを参照。

https://github.com/cilium/tetragon/tree/main/examples/tracingpolicy

イベントの filtering

tetragon が収集するイベントは多岐に渡るため、k8s クラスタ内の pod が増えるにつれて大量のイベントが収集されて tetragon pod のログサイズが増大します。この対策として tetragon ではイベント内容や対象の pod, namespace 等に基づいて収集可否を選択してログサイズを削減する設定項目が用意されています。

Export filetering

export filtering では イベントの process_execprocess_exit などの値に基づいてイベント収集の可否を選択することができます。

https://tetragon.io/docs/concepts/events/#export-filtering

これは helm chart の exportAllowListexportDenyList で指定でき、configmap としてデプロイされます。

 # Allowlist for JSON export. For example, to export only process_connect events from
  # the default namespace:
  #
  # exportAllowList: |
  #   {"namespace":["default"],"event_set":["PROCESS_EXEC"]}
  exportAllowList: |-
    {"event_set":["PROCESS_EXEC", "PROCESS_EXIT", "PROCESS_KPROBE", "PROCESS_UPROBE", "PROCESS_TRACEPOINT"]}
  # Denylist for JSON export. For example, to exclude exec events that look similar to
  # Kubernetes health checks and all the events from kube-system namespace and the host:
  #
  # exportDenyList: |
  #   {"health_check":true}
  #   {"namespace":["kube-system",""]}
  #
  exportDenyList: |-
    {"health_check":true}
    {"namespace":["", "cilium", "kube-system"]}

Field Filtering

Field filetering では収集されるイベント詳細の中で不要なフィールドを除去することでログサイズを削減することができます。こちらも helm chart で指定できます。

https://tetragon.io/docs/concepts/events/#field-filtering

k8s filtering

こちらは TracingPolicy マニフェストに含めることができるオプションであり、ポリシーを適用する対象を namespace や pod, container のラベルに基づいて選択できます。

https://tetragon.io/docs/concepts/tracing-policy/k8s-filtering/

おわりに

Tetragon では eBPF を利用した TracingPolicy という CRD を使うことでカーネルレベルでのイベントを監視したり、カーネル関数に hook を仕掛けてプロセスの kill などのアクションを実行できます。これにより Observability の 3 大要素 (logs, metrics, traces) ではカバーしきれない k8s pod のコンテナ内のプロセスレベルでのイベント監視を実現し、k8s クラスタの Observability やセキュリティをさらに強化することができます。
ドキュメントの Use Cases を元にイベント内容などをわりと詳細に見ていきましたが、調べながら記事を書いていると思ったより分量が多くなったのでユースケース 6 のうち 3 つについて検証を行いました。残りについては機会があれば書こうと思います。

Discussion