Stärke dein System mit Tetragon's eBPF-basierten Security-Observability und Runtime-Enforcement Fähigkeiten

26.10.2022Stefan Welsch
Cloud Cloud native Cilium eBPF Security Kubernetes

Wir haben euch bereits in einem früheren TechUp Cilium vorgestellt. Es handelt sich dabei um ein eBPF-basiertes Networking, Observability und Security Tool, welches in allen cloud-native Umgebungen wie beispielsweise Kubernetes eingesetzt werden kann. 🐝

Heute möchte ich einen Teil von Cilium etwas genauer betrachten, nämlich Tetragon.

Bei Tetragon handelt es sich um einen Agent, welcher in jeder Linux-Umgebung laufen kann. Dabei spielt es keine Rolle, ob es sich um eine Kubernetes Umgebung handelt oder nicht. Tetragon nutzt dabei eBPF, um Daten auszulesen und diese in verschiedensten Formaten zur Verfügung zu stellen. Um welche Daten es sich dabei handelt, werden wir später noch im Detail sehen. Tetragon ist ein Cilium-Projekt, was aber nicht bedeutet, dass es Cilium unbedingt braucht. Wir können Tetragon ohne Probleme auch als “Standalone” installieren und werden dies in diesem TechUp auch tun. Vorerst will ich euch aber ein paar Fakten zu Tetragon geben. Der erste Commit im Github wurde erst am 11. Mai 2022 gemacht. Dies spiegelt aber nicht das wirkliche Alter des Projekts wider. Tetragon ist schon seit Jahren in Cilium Enterprise enthalten. Nun wurden aber Teile des Projekts open-source der Community zur Verfügung gestellt. Tetragon ist in C und Golang geschrieben und zählt Stand heute 35 Mitwirkende.

So funktioniert Tetragon im Hintergrund

Tetragon ist, wie oben bereits erwähnt, ein eBPF-basiertes Tool welches sich um Security Observability und Runtime Enforcement kümmert. Security Observability bedeutet dabei, dass bösartige Aktivitäten in Echtzeit erkannt werden und das Reporting stattfindet, sobald dieses Event passiert. Es geht sogar soweit, dass diese bösartigen Events verhindert werden, bevor sie Schaden anrichten können. Aber wie genau soll das funktionieren? Dazu nehmen wir uns das offizielle Schaubild von Tetragon zur Hilfe.

Figure: Quelle: https://isovalent.com/blog/post/2022-05-16-tetragon (aufgerufen am 07.10.2022)

Wie wir sehen können werden verschiedene Aktivitäten wie beispielsweise Prozessausführungen, Syscall-Aktivitäten, Dateizugriffe, Namespace-Escapes, Netzwerkaktivitäten und vieles weitere von Tetragon mittels eBPF überwacht und protokolliert. Das hört sich erstmal nach jeder Menge Overhead an, nicht wahr? Tetragon macht sich hier den sogenannten SmartCollector zu nutze. Dieser filter und aggregiert bereits im Kernel die notwendigen Informationen und sendet diese erst dann an den Tetragon Agent, welcher im Userspace läuft. All diese gesammelten Daten sind aber erst nützlich, wenn man sie auch verwenden kann. Der Tetragon Agent stellt dafür beispielsweise Integrationen zu Prometheus, Grafana, fluentd und anderen Systemen zur Verfügung. Ausserdem kann man sich die Daten per JSON exportieren, um diese zu verarbeiten. Tetragon kann aber nicht nur low-level Kernel Aktivitäten beobachten, sonder auch Function Calls, Code Executions oder den Einsatz vulnerabler Bibliotheken in der Applikation überwachen. Um diese Überwachung zu starten, sind keine Änderungen im Code notwendig, da alle Daten direkt im Kernel gesammelt werden.

Wenn Tetragon in einer Kubernetes-Umgebung verwendet wird, ist es Kubernetes-aware, was bedeutet, dass es alle Kubernetes Ressourcen wie Namespaces, Pods und so weiter versteht. Die Eventerkennung kann also im Hinblick auf individuelle Workloads granular konfiguriert werden.

Wer mehr über Kernel und Userspace erfahren möchte, dem lege ich Tom’s Techup über Cilium ans Herz. Er erklärt hier sehr genau die Unterschiede.

Schauen wir uns nun ein paar Beispiele dazu an!

Real-Time Runtime Enforcement

Schauen wir uns als nächstes an, wie wir Events nicht nur erkennen und reporten können, sondern diese mittels Tetragon direkt verhindern können. Dafür bietet uns Tetragon das bereits erwähnte Runtime Enforcement. Auch hier wollen wir uns wieder ein Schaubild zur Hilfe nehmen

img

Figure: Quelle: https://isovalent.com/blog/post/2022-05-16-tetragon (aufgerufen am 07.10.2022)

Wie wir sehen können, gibt es im Kernel eine Rule Engine, in der wir Richtlinien hinterlegen können, auf welche Datei beispielsweise die Schreibrechte eingeschränkt werden sollen. Die eBPF Kernel Runtime stellt dann sicher, dass diese Richtlinien eingehalten werden. Wenn eine Applikation gegen diese Richtlinie verstösst, so kann die Aktion direkt gestoppt oder die Applikation beendet werden.

Hier sehen wir, wie so eine Regel aussehen könnte, welche verhindert dass ein Container mit Root-Rechten ausgeführt wird.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: cilium.io/v1alpha1
kind: ExecPolicy
metadata:
  name: "exec-policy-example"
spec:
  rules:
    selectors:
    # match only pod pids (AND)
    - matchPIDs:
      - operator: NotIn
        followForks: true
        isNamespacePID: true
        values:
        - 0
    # match on caps (AND)
    - matchCapabilities:
      - operator: In
        isNamespaceCapability: true
        values:
        - "CAP_SYS_ADMIN"
    # match on binaries (AND)
    - matchBinarys:
      values:
        - "*"
    # terminate the process
    - matchActions:
      - action: Sigkill

Anwendung einer Richtlinie

Der Tetragon Agent bietet uns hier verschiedene Wege an, diese Sicherheitsrichtlinen einzuspielen. Im obigen Beispiel sehen wir eine Kubernetes CRD Richtlinie. Weiterhin wären beispielsweise auch JSON Richtlinien oder Richtlinien, die von Open Policy Agent (OPA) erstellt wurden als Input möglich. Was genau passiert aber nun, wenn wir so eine Regel anlegen?

Am besten sehen wir uns das anhand von einem kleinen Beispiel an. Dazu installieren wir uns erstmal Tetragon. Eine Anleitung hierzu ist auf der Github-Seite zu finden. Auf meinem Macbook mit M1 Max ARM64-Prozessor habe ich den Container mittels kind leider nicht zum Laufen gebracht. Ich musste daher auf die GCP-Variante ausweichen.

Sobald Tetragon läuft, installieren wir uns die Demo-Applikation von Cilium.

kubectl create -f https://raw.githubusercontent.com/cilium/cilium/v1.11/examples/minikube/http-sw-app.yaml

Diese installiert uns die folgenden 4 Pods:

1
2
3
4
5
6
~ ❯ k get pods                                                                                                                                ⎈ swelsch-31014
NAME                         READY   STATUS    RESTARTS   AGE
deathstar-6f87496b94-wcpw2   1/1     Running   0          35m
deathstar-6f87496b94-wgrnc   1/1     Running   0          35m
tiefighter                   1/1     Running   0          35m
xwing                        1/1     Running   0          35m

Mit Tetragon Logs überprüfen

Schauen wir uns doch mal an, wie ein solches Event jetzt aussieht. Dazu öffnen wir ein Terminal-Fenster und überwachen dort die Logs von Tetragon.

kubectl logs -n kube-system -l app.kubernetes.io/name=tetragon -c export-stdout -f

In einem zweiten Fenster öffnen wir eine Shell in einem Pod und fragen dort einfach den aktuellen Benutzer mittels whoami ab.

1
2
3
~ ❯ kubectl exec -it xwing -- /bin/bash
# whoami
root

events

Wie wir sehen können erhalten wir als Event ein JSON-Objekt, welches verschiedenste Daten beinhaltet. Die Tetragon-CLI bietet ein Tool (observe), mit welchem wir die wesentlichen Details herauslesen könnten.

Das JSON bezüglich der Bash sieht also folgendermassen aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
{
  "process_exec": {
    "process": {
      "exec_id": "Z2tlLXN3ZWxzY2gtMzEwMTQtZGVmYXVsdC1wb29sLTI1OThkMGNlLWd0bHA6MTI5NDIyMDIzMDg3OToxMzYwNA==",
      "pid": 13604,
      "uid": 0,
      "cwd": "/",
      "binary": "/bin/bash",
      "flags": "execve rootcwd clone",
      "start_time": "2022-10-07T09:35:31.221487201Z",
      "auid": 4294967295,
      "pod": {
        "namespace": "default",
        "name": "xwing",
        "container": {
          "id": "containerd://4cb1e9a1ab98044e789c2d426909a6ee1731005cab0a1c5dea9941a5e7889c51",
          "name": "spaceship",
          "image": {
            "id": "docker.io/tgraf/netperf@sha256:8e86f744bfea165fd4ce68caa05abc96500f40130b857773186401926af7e9e6",
            "name": "docker.io/tgraf/netperf:latest"
          },
          "start_time": "2022-10-07T09:18:24Z",
          "pid": 66
        },
        "pod_labels": {
          "app.kubernetes.io/name": "xwing",
          "class": "xwing",
          "org": "alliance"
        }
      },
      "docker": "4cb1e9a1ab98044e789c2d426909a6e",
      "parent_exec_id": "Z2tlLXN3ZWxzY2gtMzEwMTQtZGVmYXVsdC1wb29sLTI1OThkMGNlLWd0bHA6MTI5NDE5NTQ2MjU3ODoxMzU5NA==",
      "refcnt": 1
    },
    "parent": {
      "exec_id": "Z2tlLXN3ZWxzY2gtMzEwMTQtZGVmYXVsdC1wb29sLTI1OThkMGNlLWd0bHA6MTI5NDE5NTQ2MjU3ODoxMzU5NA==",
      "pid": 13594,
      "uid": 0,
      "cwd": "/run/containerd/io.containerd.runtime.v2.task/k8s.io/ab719ee48fb5185a866f98ff5fabf1c0c3879e80ce3cdc6a0f52b765b72e9ec6",
      "binary": "/usr/bin/runc",
      "arguments": "--root /run/containerd/runc/k8s.io --log /run/containerd/io.containerd.runtime.v2.task/k8s.io/4cb1e9a1ab98044e789c2d426909a6ee1731005cab0a1c5dea9941a5e7889c51/log.json --log-format json exec --process /tmp/runc-process676011474 --console-socket /tmp/pty093772415/pty.sock --detach --pid-file /run/containerd/io.containerd.runtime.v2.task/k8s.io/4cb1e9a1ab98044e789c2d426909a6ee1731005cab0a1c5dea9941a5e7889c51/fc973cf9fc1302d7fdc2b0176ae80be9e246d833d921506582948010adbe81a5.pid 4cb1e9a1ab98044e789c2d426909a6ee1731005cab0a1c5dea9941a5e7889c51",
      "flags": "execve clone",
      "start_time": "2022-10-07T09:35:31.196717610Z",
      "auid": 4294967295,
      "parent_exec_id": "Z2tlLXN3ZWxzY2gtMzEwMTQtZGVmYXVsdC1wb29sLTI1OThkMGNlLWd0bHA6MjU3ODE5MjAzOTg3OjYyNDY=",
      "refcnt": 1
    }
  },
  "node_name": "gke-swelsch-31014-default-pool-2598d0ce-gtlp",
  "time": "2022-10-07T09:35:31.234770672Z"
}

Die wichtigsten Informationen, die auch observe ausspucken würde, sind:

1
process default/xwing /bin/bash

Wir sehen also, dass ein Prozess /bin/bash im Namespace default im Pod xwing gestartet wurde. Über diese Bash wollen wir nun testen, ob es möglich ist, eine Datei zu bearbeiten.

1
2
3
4
5
6
7
8
~ ❯ kubectl exec -it xwing -- /bin/bash
# vi /etc/passwd

root:x:0:0:root:/root:/bin/bash
...
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
test:x:345:345:test:/:/bin/bash

Ich erstelle einen neuen Benutzer “test” und speichere die Datei ab. Nun wollen wir testen, ob die Änderungen tatsächlich übernommen wurden.

1
2
3
$ su test
$ whoami
test

Natürlich wollen wir eine Änderung durch einen fremden Benutzer auf unserem System auf jeden Fall verhindern. Schauen wir uns nun also an, wie wir das mit Tetragon machen können.

Erstellen einer Tracing Policy

Wir erstellen uns hierfür eine Policy mit dem folgenden Inhalt. Mit dieser Policy wollen wir verhindern, dass jemand eine Datei im Ordner /tmp/forbidden anlegen kann.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: "deny-tmp-forbidden-write"
spec:
  kprobes:
  - call: "fd_install"
    syscall: false
    return: false
    args:
    - index: 0
      type: int
    - index: 1
      type: "file"
    selectors:
    - matchPIDs:
      - operator: NotIn
        followForks: true
        isNamespacePID: true 
        values:
        - 1
      matchArgs:
      - index: 1
        operator: "Prefix"
        values:
        - "/tmp/forbidden/"
      matchActions:
      - action: FollowFD
        argFd: 0
        argName: 1
  - call: "__x64_sys_close"
    syscall: true
    args:
    - index: 0
      type: "int"
    selectors:
    - matchActions:
      - action: UnfollowFD
        argFd: 0
        argName: 0
  - call: "__x64_sys_read"
    syscall: true
    args:
    - index: 0
      type: "fd"
    - index: 1
      type: "char_buf"
      returnCopy: true
    - index: 2
      type: "size_t"
  - call: "__x64_sys_write"
    syscall: true
    args:
    - index: 0
      type: "fd"
    - index: 1
      type: "char_buf"
      sizeArgIndex: 3
    - index: 2
      type: "size_t"
    selectors: 
    - matchActions:
        - action: Sigkill

Im Wesentlichen passiert hier folgendes. Sobald ein Syscall __x64_sys_write ausgeführt wird, wird geschaut, ob es sich dabei um ein “write” auf eine Datei im Ordner /tmp/forbidden handelt. Ist dies der Fall, so soll das Sigkill-Event ausgeführt werden, was in unserem Fall bedeutet, dass der Schreibprozess sofort beendet wird.

Testen der Tracing Policy

Um zu beweisen das unsere Policy auch funktioniert, legen wir uns als Erstes eine Datei an, welche wir später editieren.

1
2
3
$ touch /tmp/forbidden/donotchange.me
$ ls /tmp/forbidden/
donotchange.me

Nun versuchen wir, die Datei zu editieren, aber erstmal ohne die angegebene Policy. Wir schreiben einfach “File changed” in die Datei und speichern und schliessen die Datei.

1
2
$ vi /tmp/forbidden/donotchange.me
File changed

Wir können die Datei editieren und sehen unsere Änderungen, wenn wir diese wieder öffnen. Probieren wir das gleiche nun mit der oben gezeigten Policy. Wir führen dafür den folgenden Befehl auf unserem Host-System aus.

1
k apply -f deny_tmp_forbidden_write.yaml

Anschliessend eröffnen wir wieder eine Bash-Shell im Container und versuchen, die Datei im /tmp-Ordner zu editieren. Wir schreiben unter “File changed” einfach irgendwas drunter und versuchen, die Datei wieder zu speichern und zu schliessen.

1
2
3
4
5
vi /tmp/forbidden/donotchange.me
File changed
File changed again

:wqKilled

Wie wir sehen können, wird der Schreibprozess sofort beendet. Unsere Policy erfüllt also wie gewünscht ihren Zweck. So sieht das ganze schön zusammengefasst und übersichtlich in der Tetragon-CLI aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ kubectl logs -n kube-system -l app.kubernetes.io/name=tetragon -c export-stdout -f | tetra getevents -o compact

🚀 process default/xwing /bin/bash
🚀 process default/xwing /bin/ls
💥 exit    default/xwing /bin/ls  0
🚀 process default/xwing /usr/bin/vi donotchange.me
📬 open    default/xwing /usr/bin/vi /tmp/forbidden/donotchange.me
📪 close   default/xwing /usr/bin/vi
📬 open    default/xwing /usr/bin/vi /tmp/forbidden/donotchange.me
📝 write   default/xwing /usr/bin/vi /tmp/forbidden/donotchange.me 32 bytes
💥 exit    default/xwing /usr/bin/vi donotchange.me SIGKILL

Fazit

Mit Tetragon haben wir ein sehr mächtiges Mittel, um unsere Systeme direkt im Kernel abzusichern. Natürlich ist es für die Anwendung in Kombination mit Cilium nicht notwendig, Policies auf dieser Ebene zu definieren, aber es ist auf jeden Fall gut zu wissen, wie es funktioniert.

Wir bei b-nova werden uns auf jeden Fall in naher Zukunft weitere Projekte im Zusammenhang mit eBPF anschauen, also bleibe gespannt und folge unseren Social-Media Kanälen. Stay tuned! 🔥

Stefan Welsch

Stefan Welsch – Manitu, Pionier, Stuntman, Mentor. Als Gründer von b-nova ist Stefan immer auf der Suche nach neuen und vielversprechenden Entwicklungsfeldern. Er ist durch und durch Pragmatiker und schreibt daher auch am liebsten Beiträge die sich möglichst nahe an 'real-world' Szenarien anlehnen.