Strengthen your system with Tetragon's eBPF-based Security Observability and Runtime Enforcement capabilities

26.10.2022Stefan Welsch
Cloud Cloud native Cilium eBPF Security Kubernetes

We have already introduced Cilium to you in a previous TechUp. It’s an eBPF-based networking, observability and security tool that can be used in all cloud-native environments such as, for example, Kubernetes. 🐝

Today, I want to take a closer look at a part of Cilium, namely Tetragon.

Tetragon is an agent which can run in any Linux environment. It doesn’t matter if it’s a Kubernetes environment or not. Tetragon uses eBPF to read data and make it available in various formats. We will later see in detail what kind of data we’re talking about. Tetragon is a Cilium project, but this doesn’t mean that it necessarily needs Cilium. We can install Tetragon as a standalone without any problems and will do so in this TechUp. But first, let me give you some facts about Tetragon. The first commit on Github was made as recently as May 11, 2022. But this doesn’t reflect the real age of the project. Tetragon has been included in Cilium Enterprise for years. But now, parts of the project have been open-sourced to the community. Tetragon is written in C and Golang and has 35 contributors as of today.

How Tetragon works in the background

Tetragon is, as mentioned above, an eBPF-based tool that takes care of security observability and runtime enforcement. Security observability means that malicious activities are detected in real-time and reporting takes place as soon as an event occurs. In fact, it goes so far as that these malicious events can be stopped before they can do any damage. But how exactly does this work? For that, let’s take a look at Tetragon’s official diagram.

Figure: Source: https://isovalent.com/blog/post/2022-05-16-tetragon (10/7/2022)

As we can see, various activities such as process executions, syscall activities, file access, namespace escapes, network activities and many other activities are monitored and logged by Tetragon using eBPF. At first glance, that sounds like a lot of overhead, right? Tetragon makes use of the so-called SmartCollector. It filters and aggregates the necessary information in the kernel and then sends it to the Tetragon agent running in user space. All this collected data is only useful if you’re able to use it. The Tetragon client provides integrations to Prometheus, Grafana, fluentd and other systems. Furthermore, you can export the data via JSON to process it. Tetragon can’t only monitor low-level kernel activities, but also function calls, code executions or the use of vulnerable libraries in the application. To start this monitoring, no changes in the code are necessary, since all data is collected directly in the kernel. Pretty cool, right?

When used in a Kubernetes environment, Tetragon is Kubernetes-aware, which means it understands all Kubernetes resources such as namespaces, pods and so on. Thus, event detection can be configured on a granular level with respect to individual workloads.

If you want to know more about kernel and userspace, I recommend Tom’s TechUp about Cilium. He explains the differences very precisely.

Now let’s take a look at a few examples!

Real-Time Runtime Enforcement

Let’s look at how we can not only detect and report events, but also directly prevent them using Tetragon. For this purpose, Tetragon offers us the already mentioned Runtime Enforcement. Again, we’ll take a look at the following diagram to help us understand this.

img

Figure: Source: https://isovalent.com/blog/post/2022-05-16-tetragon (10/7/2022)

As we can see, there’s a rule engine within the kernel, in which we can store policies, for example for which file the write permissions should be restricted. The eBPF kernel runtime then ensures that these guidelines are followed. If an application violates this policy, the action can immediately be stopped or the application can be terminated.

Here we see what such a rule that prevents a container from being used with root privileges could look like.

 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

How to apply a policy

The Tetragon Agent provides us with several ways to inject these security policies. In the example above we see a Kubernetes CRD policy. Furthermore, JSON policies or policies created by Open Policy Agent (OPA) would also be possible as inputs. But what exactly happens when we create such a rule?

The best way to see this is by means of a small example. First we install Tetragon. Instructions can be found on the Github page. On my Macbook with the M1 Max ARM64 processor I unfortunately couldn’t get the container to work using kind. I therefore had to switch to the GCP variant.

Once Tetragon is up and running, let’s install the demo application from Cilium.

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

This installs the following 4 pods for us:

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

Checking logs with Tetragon

Let’s now take a look at what such an event might look like. For this, we open a terminal window and check the Tetragon logs.

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

In a second window we open a shell in a pod and simply check who’s the current user using whoami.

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

events

As we can see, we get a JSON object as an event, which contains various data. The Tetragon CLI provides a tool called (observe) with which we could extract only the essential details.

The nicely formatted JSON from the bash looks like this:

 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"
}

The most important information that observe would also return is:

1
process default/xwing /bin/bash

So we see that a process /bin/bash has been started in the namespace default in the pod xwing. Via this bash, we now want to test if it’s possible to edit a file.

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

Let’s create a new user “test” and save the file. Now we want to test whether the changes were actually applied.

1
2
3
$ su test
$ whoami
test

Of course, we want to prevent a change by a foreign user on our system at all costs. So let’s now look at how we can do that with Tetragon.

Creating a tracing policy

With this policy we want to prevent that someone can create a file in the folder /tmp/forbidden. For this, we create a policy with the following content.

 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

Basically what happens here is the following. As soon as an __x64_sys_write syscall is executed, it checks if it’s a write to a file in the /tmp/forbidden folder. If this is the case, the sigkill event must be executed, which in our case means that the write process is terminated immediately.

Testing the tracing policy

To prove that our policy works, we first create a file, which we’ll edit later.

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

Now, we try to edit the file, but first without the specified policy. We simply write “File changed” in the file and save and close the file.

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

We can edit the file and see our changes when we open it again. Now let’s try the same thing with the policy shown above. We run the following command on our host system to do this.

1
$ k apply -f deny_tmp_forbidden_write.yaml

Then we open a bash shell in the container again and try to edit our file in the /tmp folder. We just write something below “File changed” and try to save and close the file again.

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

:wqKilled

As we can see, the writing process is terminated immediately. Our policy therefore fulfills its purpose as desired. This is what the whole thing looks like nicely summarized and neatly arranged in the Tetragon CLI:

 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

Conclusion

With Tetragon we have a very powerful tool to secure our systems directly in the kernel. Of course, for usage in combination with Cilium it is not necessary to define policies on this level, but it is definitely good to know how it works.

We at b-nova will definitely be looking at more projects related to eBPF in the near future, so stay tuned and follow our social media channels! 🔥

Stefan Welsch

Stefan Welsch – 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.