In the first part, we provided an overview of Tetragon. We also installed Tetragon on a standalone Linux host to get hands-on experience with Tetragon.
In this second part, we’ll continue our journey with Tetragon by looking at how to install it on Kubernetes and the various components deployed during the installation process.
We’ll wrap up by looking at Tetragon events in JSON format and creating our first tracing policy!
Installing Tetragon on Kubernetes#
Prerequisites and KinD Installation#
Tetragon can be installed on any Kubernetes cluster with eBPF support. In this article, we’ll install it on KinD (Kubernetes in Docker).

KinD is a tool that allows you to run Kubernetes clusters locally. It uses Docker containers to emulate the cluster nodes.
Unlike a traditional Kubernetes cluster, all KinD nodes share the same Linux kernel: the one from your host machine. Thus, regardless of the number of nodes created, there is only one kernel.
As we saw in the previous section, Tetragon is based on eBPF programs, which run directly in the Linux kernel.
Therefore, a single KinD cluster is enough to test Tetragon.
To install KinD, simply type:
VERSION=v0.32.0
[ $(uname -m) = x86_64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/$VERSION/kind-linux-amd64
[ $(uname -m) = aarch64 ] && curl -Lo ./kind https://kind.sigs.k8s.io/dl/$VERSION/kind-linux-arm64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kindIf you encounter any issues, feel free to check out the official website.
Creating the KinD Cluster#
I based this section on the official documentation.
Let’s create this configuration file to set up the cluster:
1apiVersion: kind.x-k8s.io/v1alpha4
2kind: Cluster
3nodes:
4 - role: control-plane
5 extraMounts:
6 - hostPath: /proc
7 containerPath: /procHostextraMounts allows you to create additional mounts between the host and the KinD cluster.
Here, we mount the host machine’s /proc directory into the Kubernetes node. This allows access to actual system information (processes, PIDs, etc.).
Without this mount, Tetragon would only see the Docker container’s /proc, which would skew the events observed via eBPF.
Next, we create the Kubernetes cluster:
kind create cluster --config kind-config.yamlInstalling Tetragon#
We’ll use Helm to install Tetragon. To specify the host machine’s /proc, we’ll pass an option to Helm:
EXTRA_HELM_FLAGS=(--set tetragon.hostProcPath=/procHost)The Tetragon installation otherwise uses the default options:
helm repo add cilium https://helm.cilium.io
helm repo update
helm install tetragon ${EXTRA_HELM_FLAGS[@]} cilium/tetragon -n kube-systemTo find out when Tetragon is finally deployed, you can run the following command:
kubectl rollout status -n kube-system ds/tetragon -wHow Tetragon Is Installed on Kubernetes#
Let’s try to understand how Tetragon works on Kubernetes.
Let’s take a look at what we’ve installed with Helm.
Pods#
Let’s start by looking at the pods:
kubectl get pod -n kube-system -l app.kubernetes.io/instance=tetragonNAME READY STATUS RESTARTS AGE
tetragon-operator-59bdd59db8-8865c 1/1 Running 0 47s
tetragon-vjgfb 2/2 Running 0 47sThere are two different pods:
tetragon-*, which is installed on each node via a DaemonSet. This is the Tetragon agent.tetragon-operator-*, which is installed via a Deployment. This is the Tetragon operator.
Let’s take a closer look at the role of each.
The Tetragon Agent#
We can also see that the tetragon pod has two containers running simultaneously. Let’s look at the names of these containers:
kubectl get daemonset tetragon -n kube-system -o jsonpath='{.spec.template.spec.containers[*].name}'
export-stdout tetragon- The export-stdout container exposes the events generated by Tetragon
- The tetragon container is responsible for installing and running eBPF programs
The agent is the component that actually monitors the system on each node.
The Tetragon Operator#
The operator plays a control role:
- It monitors Tracing Policies
- It translates these policies into instructions
- It instructs the agents to install the corresponding eBPF programs
It therefore serves as the link between the Kubernetes configuration and execution on each node.
CRDs#
As we’ve just seen, the operator manages tracing policies. But how do you install them on Kubernetes? Configuration is done via CRDs.
Let’s take a look at the CRDs that have been installed:
kubectl get crdThe command returns the following:
NAME CREATED AT
tracingpolicies.cilium.io 2026-06-16T12:44:59Z
tracingpoliciesnamespaced.cilium.io 2026-06-16T12:45:02ZThere are two CRDs:
- Tracing policies that apply to the entire cluster
- Namespaced tracing policies that apply, as the name suggests, to a Kubernetes namespace.
Tetragon Up and Running#
Now that we’ve seen what we’re actually installing in Kubernetes with Tetragon, let’s take a look at how to use it.
Status#
You can now verify that Tetragon is running properly:
kubectl exec -n kube-system ds/tetragon -c tetragon -- \
tetra statusYou should see:
Health Status: runningLogs#
Now let’s monitor an nginx pod with Tetra:
kubectl exec -n kube-system ds/tetragon -c tetragon -- \
tetra getevents -o compact --pods nginxSince this pod does not yet exist, there are no logs yet.
In another terminal, we create the nginx pod:
kubectl run --image nginx nginxHere’s what we see:
🚀 process default/nginx /usr/bin/basename /docker-entrypoint.d/20-envsubst-on-templates.sh
💥 exit default/nginx /usr/bin/basename /docker-entrypoint.d/20-envsubst-on-templates.sh 0
🚀 process default/nginx /usr/bin/awk -v filter= "END { for (name in ENVIRON) { print ( name ~ filter ) ? name : "" } }"
💥 exit default/nginx /usr/bin/awk -v filter= "END { for (name in ENVIRON) { print ( name ~ filter ) ? name : "" } }" 0
💥 exit default/nginx /docker-entrypoint.d/20-envsubst-on-templates.sh /docker-entrypoint.d/20-envsubst-on-templates.sh 0
🚀 process default/nginx /usr/bin/basename /docker-entrypoint.d/30-tune-worker-processes.sh
💥 exit default/nginx /usr/bin/basename /docker-entrypoint.d/30-tune-worker-processes.sh 0
🚀 process default/nginx /kind/bin/mount-product-files.sh /kind/bin/mount-product-files.sh
🚀 process default/nginx /usr/bin/jq -r .bundle
💥 exit default/nginx /usr/bin/jq -r .bundle 0
etcThis shows all the different binaries that are executed when the nginx pod starts up.
We’ve already seen this output in the previous section. Now let’s take a look at the default log. To do this, we need to run the tetra command again:
kubectl exec -n kube-system ds/tetragon -c tetragon -- \
tetra getevents --pods nginx | jqWe’re using the jq command to make the JSON a little easier to read.
Now let’s connect to the pod:
kubectl exec -it nginx -- bashIn the Tetra CLI, we then see:
1{
2 "process_exec": {
3 "process": {
4 "exec_id": "a2luZC1jb250cm9sLXBsYW5lOjExNTEwNDY3OTYwNTY6NjQwOQ==",
5 "pid": 6409,
6 "uid": 0,
7 "cwd": "/",
8 "binary": "/usr/bin/bash",
9 "flags": "execve rootcwd clone",
10 "start_time": "2026-06-16T13:29:38.388236227Z",
11 "auid": 4294967295,
12 "pod": {
13 "namespace": "default",
14 "name": "nginx",
15 "uid": "82d08485-bc2a-45ce-bf99-85e255fc5903",
16 "container": {
17 "id": "containerd://f29206e762bd035592b1ba0cc14a3d6deafa8dcc98bb974eaf5ca1ef38dffbea",
18 "name": "nginx",
19 "image": {
20 "id": "docker.io/library/nginx@sha256:608a100c71651bf5b773c89083b4a1ad7ef4b2bd05d7a7e552271e03123692ad",
21 "name": "docker.io/library/nginx:latest"
22 },
23 "start_time": "2026-06-16T13:22:31Z",
24 "pid": 40,
25 "security_context": {}
26 },
27 "pod_labels": {
28 "run": "nginx"
29 },
30 "workload": "nginx",
31 "workload_kind": "Pod"
32 },
33 "docker": "f29206e762bd035592b1ba0cc14a3d6",
34 "parent_exec_id": "a2luZC1jb250cm9sLXBsYW5lOjcwMDgyMTgyNjc0Nzo1NTQ3",
35 "tid": 6409,
36 "in_init_tree": false
37 },
38 "parent": {
39 "exec_id": "a2luZC1jb250cm9sLXBsYW5lOjcwMDgyMTgyNjc0Nzo1NTQ3",
40 "pid": 5547,
41 "uid": 0,
42 "cwd": "/run/containerd/io.containerd.runtime.v2.task/k8s.io/65418056615070bc5892ebbd73d5cc97f039f389a88c2fc06232f815ecc9c96e",
43 "binary": "/usr/local/bin/containerd-shim-runc-v2",
44 "arguments": "-namespace k8s.io -id 65418056615070bc5892ebbd73d5cc97f039f389a88c2fc06232f815ecc9c96e -address /run/containerd/containerd.sock",
45 "flags": "execve clone",
46 "start_time": "2026-06-16T13:22:17.386071799Z",
47 "auid": 4294967295,
48 "parent_exec_id": "a2luZC1jb250cm9sLXBsYW5lOjcwMDgxMzkzNTEwOTo1NTQw",
49 "tid": 5547,
50 "in_init_tree": false
51 }
52 },
53 "node_name": "kind-control-plane",
54 "time": "2026-06-16T13:29:38.388235451Z",
55 "node_labels": {
56 "beta.kubernetes.io/arch": "amd64",
57 "beta.kubernetes.io/os": "linux",
58 "kubernetes.io/arch": "amd64",
59 "kubernetes.io/hostname": "kind-control-plane",
60 "kubernetes.io/os": "linux",
61 "node-role.kubernetes.io/control-plane": ""
62 }
63}We can see a whole lot of data that wasn’t visible in the previous standard output. We won’t explain every field, but let’s take a look at the most important ones—or at least the ones that seemed the least straightforward to me.
process_exec: indicates that a process has been launched. You can see which node it’s running on vianode_name, the launch time withtime, and also the container runtime that launched the container withparent.processprovides details about the launched process: for example, which container in which podexec_idis an identifier that allows you to track this process until it terminates. It’s in Base64:
echo 'a2luZC1jb250cm9sLXBsYW5lOjExNTEwNDY3OTYwNTY6NjQwOQ==' | base64 -d
kind-control-plane:1151046796056:64091151046796056corresponds to the time in nanoseconds from when the machine started up until the process started (a monotonic kernel timestamp)
Let’s Build My First Tracing Policy#
To wrap up this chapter, we’re going to write a tracing policy. Its purpose? To prevent the file /var/run/secrets/kubernetes.io/serviceaccount/token from being read.
To do this, we’ll build it step by step.
The World’s Simplest Tracing Policy#
This first tracing policy will allow us to monitor all read accesses to the system. To do this, we will monitor the openat system call (syscall).
1apiVersion: cilium.io/v1alpha1
2kind: TracingPolicy
3metadata:
4 name: "sys-openat"
5spec:
6 kprobes:
7 - call: "sys_openat"Let’s quickly break down the lines:
kprobesare a type of eBPF program that allow you to observe all functions (symbols, to be precise) in the Linux kernel;calllets you define the function you want to observe.
This way, we’ll see a message every time the sys_openat function is called in the Linux kernel. This function is triggered every time the openat syscall
is initiated.
Let’s check it out:
kubectl apply -f tp-openat-basic.yamlLet’s run tetra again in a more readable way:
kubectl exec -n kube-system ds/tetragon -c tetragon -- \
tetra getevents -o compact --pods nginxIn another terminal, let’s connect to the nginx pod:
kubectl exec -it nginx -- bashWhat do we see in the Tetragon logs?
🚀 process default/nginx /usr/bin/bash
📬️ openat default/nginx /usr/bin/bash
📬️ openat default/nginx /usr/bin/bash
📬️ openat default/nginx /usr/bin/bash
📬️ openat default/nginx /usr/bin/bash
📬️ openat default/nginx /usr/bin/bash
📬️ openat default/nginx /usr/bin/bash
📬️ openat default/nginx /usr/bin/bash
📬️ openat default/nginx /usr/bin/bash
📬️ openat default/nginx /usr/bin/bash
📬️ openat default/nginx /usr/bin/bash
📬️ openat default/nginx /usr/bin/bash
📬️ openat default/nginx /usr/bin/bashWe can see that when bash starts up, it reads quite a few files. But we can’t see which ones.
Tracing policy—a bit more complicated#
To do this, we’ll need to modify the tracing policy.
Let’s look at the signature of the sys_open_at() function:
int sys_openat(int dirfd, const char *path, int flags, mode_t mode)You need to know how to read a C function:
int: corresponds to the return code of theopen_atsyscall => we’re not interested in this.int dirfd: corresponds to a file descriptor. It is an integer (int).const char *path: corresponds to the path of the file being read. It is of typeconst char*orstringin high-level languages.
This is exactly the parameter we want! So we’ll need to add the first two arguments to the previous files.
Therefore, we’ll use args:
1apiVersion: cilium.io/v1alpha1
2kind: TracingPolicy
3metadata:
4 name: "sys-openat"
5spec:
6 kprobes:
7 - call: "sys_openat"
8 args:
9 - index: 0 #dirfd
10 type: int
11 - index: 1 #path
12 type: stringindexcorresponds to the argument’s index (starting at 0!)typecorresponds to the argument’s type
This can be interpreted as:
- The first argument is a file descriptor; it is of type integer
- The second argument is the path to the file being read; it is of type string.
Let’s check:
kubectl replace -f tp-openat-args.yamlLet’s reconnect to the nginx pod; we now know exactly which files are being read:
🚀 process default/nginx /usr/bin/bash
📬️ openat default/nginx /usr/bin/bash /etc/ld.so.cache
📬️ openat default/nginx /usr/bin/bash /lib/x86_64-linux-gnu/libtinfo.so.6
📬️ openat default/nginx /usr/bin/bash /lib/x86_64-linux-gnu/libc.so.6
📬️ openat default/nginx /usr/bin/bash /etc/nsswitch.conf
📬️ openat default/nginx /usr/bin/bash /etc/passwd
📬️ openat default/nginx /usr/bin/bash /etc/bash.bashrc
📬️ openat default/nginx /usr/bin/bash /root/.bashrc
📬️ openat default/nginx /usr/bin/bash /root/.bash_history
📬️ openat default/nginx /usr/bin/bash /usr/share/terminfo/x/xterm
📬️ openat default/nginx /usr/bin/bash /root/.inputrc
📬️ openat default/nginx /usr/bin/bash /etc/inputrcNow let’s verify that we have access to the /var/run/secrets/kubernetes.io/serviceaccount/token file in the pod:
cat /var/run/secrets/kubernetes.io/serviceaccount/tokeneyJhbGciOiJSUzI1NiIsImtpZCI6Ik9VRTBaVEFUTXNNaTRPOUZFTkZrbHRWVnZIQ0JfeUt1S0RRd2lLUnFtYncifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxODA4Mjc4OTA1LCJpYXQiOjE3NzY3NDI5MDUsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiMWI1ZGEwMjUtYzk2ZS00OTJjLWI2MTItZGE4YTUyY2ZmN2E1Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwibm9kZSI6eyJuYW1lIjoia2luZC1jb250cm9sLXBsYW5lIiwidWlkIjoiZmNmYmU3M2EtYzhhMy00YzRmLWFiMjgtZWZhYTM4MDNiNTgwIn0sInBvZCI6eyJuYW1lIjoibmdpbngiLCJ1aWQiOiIxYWU2NmU4YS02MWFjLTQ5NmEtYWQ3My03OGNlNmNlN2JlNTgifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiIzNTAwZWY5NC00MWU4LTQ2YjctYmE4ZC1jMmVmZjdkZmY5YmQifSwid2FybmFmdGVyIjoxNzc2NzQ2NTEyfSwibmJmIjoxNzc2NzQyOTA1LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpkZWZhdWx0In0.JdkU6I6CJ3DCZDgC3iDfPy9MOC6Y60a5E0RKmkhJduC9j8hqmfR1wssESiYmrx_QRA5YhnOY6qARJZfIEKM9g_XQr_ZvI7MBjiRduqofROWZz5gMpFt_E1FBZTFbZsplc2QWuylAUozo0EUha56oFfM5TYimKPbJfOTywijHnbcZcXAvvZX9GLyAIu20rgzK9iIdqj1dSa89K48To_uGZIaaQUhvrF7dh4PSJ67Xuvp2qOA3Cq-xkdbxO-GKmAGasKfoJq5GCvAvQOxc6Rx2iXBbFKnPpTjMzvI0Xfx19_9e5I7l1XhYexVgkHBtPHBTri9eKMxVqEvRQm69g-TgLQHow can we prevent this?
Final tracing policy#
To do this, simply reuse the previous tracing policy and send a SIGKILL to the process when the path matches the desired value:
1apiVersion: cilium.io/v1alpha1
2kind: TracingPolicy
3metadata:
4 name: "sys-openat"
5spec:
6 kprobes:
7 - call: "sys_openat"
8 args:
9 - index: 0 #dirfd
10 type: int
11 - index: 1 #path
12 type: string
13 selectors:
14 - matchArgs:
15 - index: 1 #path
16 operator: "Equal"
17 values:
18 - "/var/run/secrets/kubernetes.io/serviceaccount/token"
19 matchActions:
20 - action: SigkillThis policy is relatively straightforward: it checks the second argument, which corresponds to the file path; if it matches the value /var/run/secrets/kubernetes.io/serviceaccount/token,
it sends the Sigkill signal to it.
Let’s check:
kubectl replace -f tp-openat-enforcement.yamlkubectl exec -it nginx -- bash
cat /var/run/secrets/kubernetes.io/serviceaccount/tokenThe output is then:
KilledIn Tetragon, we observe:
🚀 process default/nginx /usr/bin/bash
🚀 process default/nginx /usr/bin/cat /var/run/secrets/kubernetes.io/serviceaccount/token
📬️ openat default/nginx /usr/bin/cat /var/run/secrets/kubernetes.io/serviceaccount/token
💥 exit default/nginx /usr/bin/cat /var/run/secrets/kubernetes.io/serviceaccount/token SIGKILLThat’s exactly what we wanted to do.
We continued exploring Tetragon by covering:
- installing Tetragon on a Kubernetes cluster;
- how Tetragon works in a Kubernetes environment;
- Tetragon’s JSON-formatted logs;
- a first introduction to TracingPolicy.
In the next article, we’ll dive deeper into TracingPolicy.




