Aller au contenu
  1. Tetragons/

Installons Tetragon dans Kubernetes

·2309 mots·11 mins·
Joseph Ligier
Auteur
Joseph Ligier
CNCF ambassador | Kubestronaut 🐝
Sommaire
Ma première fois avec Tetragon - Cet article fait partie d'une série.
Partie 2: Cet article

Dans la première partie, nous avons eu une vue globale de Tetragon. Nous avons également installé Tetragon sur un Linux tout seul pour comprendre de façon concrète à quoi sert cet outil.

Dans cette deuxième partie, nous allons continuer ce voyage avec Tetragon en voyant son installation dans Kubernetes, les différents composants qui sont alors installés.

On finira en observant un pod avec des logs json et en créant notre première tracing policy !


Installation de Tetragon sur Kubernetes
#

Limite et installation de KinD
#

Nous pouvons installer Tetragon dans tous les clusters Kubernetes où eBPF fonctionne. Nous allons l’installer sur KinD (Kubernetes in Docker).

Kind logo

KinD est une distribution Kubernetes permettant de tester des applications Kubernetes sur la même machine, elle repose sur Docker pour émuler les nœuds du cluster.

Par conséquent, contrairement à un Kubernetes classique, tous les nœuds KinD partagent un unique noyau Linux, celui de votre hôte. Ainsi quel que soit le nombre de nœuds créés, il n’y a qu’un seul noyau.

Kind archi
le noyau linux partage tous les nœuds dans Kind

Comme nous l’avons vu dans la partie précédente, Tetragon est basé sur eBPF, des programmes s’exécutant sur le noyau Linux.

Ainsi il est inutile pour tester Tetragon d’installer sur plus d’un nœud avec KinD.

lab Si vous créez une tracing policy sur KinD, elle s’appliquera également à votre machine qui héberge votre cluster Kubernetes et pas uniquement aux conteneurs du cluster Kubernetes.

Pour installer KinD, il suffit de taper :

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/kind

En cas de problème, n’hésitez pas à consulter le site officiel.

Création du cluster KinD
#

Je me suis basé sur la doc officiel pour écrire cette partie.

Créons ce fichier de configuration pour créer ce cluster :

kind-config.yaml
1apiVersion: kind.x-k8s.io/v1alpha4
2kind: Cluster
3nodes:
4  - role: control-plane
5    extraMounts:
6      - hostPath: /proc
7        containerPath: /procHost
lab

extraMounts permet de créer des montages supplémentaires entre l’hôte et le cluster KinD.

Ici, on monte le répertoire /proc de la machine hôte dans le node Kubernetes. Cela permet d’accéder aux informations réelles du système (processus, PIDs, etc.).

Sans ce montage, Tetragon verrait uniquement le /proc du conteneur Docker, ce qui fausserait les événements observés via eBPF.

On crée alors le cluster Kubernetes :

kind create cluster --config kind-config.yaml

Installation de Tetragon
#

Nous allons utiliser helm pour installer Tetragon. Pour indiquer le /proc de la machine hôte, nous allons donner une option à helm :

EXTRA_HELM_FLAGS=(--set tetragon.hostProcPath=/procHost)

L’installation de Tetragon utilise sinon les options par défaut :

helm repo add cilium https://helm.cilium.io
helm repo update
helm install tetragon ${EXTRA_HELM_FLAGS[@]} cilium/tetragon -n kube-system
lab Pour une installation en production, je conseillerais d’installer Tetragon dans un autre namespace que kube-system. Pour customiser l’installation, n’hésitez pas à consulter la chart helm.

Pour savoir quand Tetragon est enfin déployé, vous pouvez lancer la commande suivante :

kubectl rollout status -n kube-system ds/tetragon -w

Comment Tetragon est installé sur Kubernetes
#

On va essayer de comprendre comment fonctionne Tetragon sur Kubernetes.

Kube archi

Regardons ce qu’on a installé avec helm.

Les pods
#

Regardons déjà les pods :

kubectl get pod -n kube-system -l app.kubernetes.io/instance=tetragon
NAME                                 READY   STATUS    RESTARTS   AGE
tetragon-operator-59bdd59db8-8865c   1/1     Running   0          47s
tetragon-vjgfb                       2/2     Running   0          47s

On a deux pods différents :

  • tetragon-* , il est installé sur chaque nœud avec un DaemonSet. C’est un agent Tetragon.
  • tetragon-operator-*, il est installé avec un Deployment. C’est l’opérateur Tetragon.

Détaillons le role de chacun.

L’agent Tetragon
#

On voit également que le pod tetragon a deux containers qui tournent en même temps. Regardons le nom de ces containers :

kubectl get daemonset tetragon -n kube-system -o jsonpath='{.spec.template.spec.containers[*].name}'
export-stdout tetragon
  • le container export-stdout expose les événements générés par Tetragon
  • le container tetragon est responsable de l’installation et de l’exécution des programmes eBPF

L’agent est le composant qui observe réellement le système sur chaque nœud.

L’opérateur Tetragon
#

L’opérateur a un rôle de contrôle :

  • Il surveille les Tracing Policies
  • Il traduit ces policies en instructions
  • Il demande aux agents d’installer les programmes eBPF correspondants

C’est donc lui qui fait le lien entre la configuration Kubernetes et l’exécution sur chacun des nœuds.

Les CRDs
#

Comme nous venons le voir l’opérateur gère les tracings policies. Mais comment les installer sur Kubernetes ? La configuration se fait via des CRDs.

Regardons les CRDs qui ont été installées :

kubectl get crd

La commande répond alors :

NAME                                  CREATED AT
tracingpolicies.cilium.io             2026-06-16T12:44:59Z
tracingpoliciesnamespaced.cilium.io   2026-06-16T12:45:02Z

Il y a deux CRDs :

  • Les tracing policies qui s’appliquent à tout le cluster
  • Les tracing policies namespacé qui s’appliquent, comme son nom l’indique, à un namespace Kubernetes.
Si vous avez déjà regardé ce qu’installait Cilium dans un cluster Kubernetes, l’architecture est quasi identique : un agent cilium par nœud pour installer les programmes eBPF, un opérateur qui est le liant entre eBPF et Kubernetes et des CRDs pour les cilium network policy

Tetragon up and Running
#

Maintenant qu’on a vu ce qu’on installait réellement dans Kubernetes avec Tetragon, regardons comment l’utiliser.

Status
#

Vous pouvez déjà vérifier que Tetragon fonctionne bien :

kubectl exec -n kube-system ds/tetragon -c tetragon -- \
tetra status

Si cela affiche :

Health Status: running

Les logs
#

On va maintenant observer un pod nginx avec tetra :

kubectl exec -n kube-system ds/tetragon -c tetragon -- \
tetra getevents -o compact --pods nginx

Ce pod n’existant pas encore, il n’y a pas encore de log.

Sur un autre terminal, on crée le pod nginx :

kubectl run --image nginx nginx

Voilà ce qu’on voit :

🚀 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
etc

On voit ainsi tous les différents binaires qui sont exécutés au démarrage du pod nginx.

On a déjà vu cet affichage dans la partie précédente. Je vous propose maintenant de regarder le log par défaut. Il faut donc relancer la commande tetra :

kubectl exec -n kube-system ds/tetragon -c tetragon -- \
tetra getevents --pods nginx | jq

Nous utilisons la commande jq qui permet de rendre un peu plus joli le json.

Maintenant connectons nous au pod :

kubectl exec -it nginx -- bash

Du côté de la tetra cli, on voit alors :

tetra cli output
 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}

On voit tout un tas de données qu’on ne voyait pas avec la sortie standard précédente. Nous n’allons pas expliquer tous les champs mais regardons quand même les plus importants ou ceux qui m’ont paru le moins trivial à comprendre.

  • process_exec: signifie qu’un processus est lancé. On peut voir sur quel nœud via node_name, l’heure de lancement avec time mais également le container runtime qui a lancé le container avec parent.
  • process donne les détails sur le processus lancé : sur quel container de quel pod par exemple
  • exec_id est un identifiant qui va permettre de suivre ce processus jusqu’à ce qu’il s’éteigne. C’est en base64 :
echo 'a2luZC1jb250cm9sLXBsYW5lOjExNTEwNDY3OTYwNTY6NjQwOQ==' | base64 -d
kind-control-plane:1151046796056:6409
  • 1151046796056 correspond à la durée en nanoseconde depuis le démarrage de la machine jusqu’au démarrage du processus (un timestamp monotonic du kernel)

Construisons ma première tracing policy
#

Pour finir ce chapitre, nous allons écrire une tracing policy. Son but ? Empêcher de lire le fichier /var/run/secrets/kubernetes.io/serviceaccount/token.

Ce fichier contient un token pour atteindre l’API de Kubernetes.

Pour cela, nous allons la construire progressivement.

La plus simple tracing policy du monde
#

Cette première tracing policy va permettre d’observer tous les accès en lecture du système. Pour cela nous allons observer l’appel système openat (syscall).

tp-openat-basic.yaml
1apiVersion: cilium.io/v1alpha1
2kind: TracingPolicy
3metadata:
4  name: "sys-openat"
5spec:
6  kprobes:
7  - call: "sys_openat"

Détaillons rapidement les lignes :

  • kprobes sont un type de programme eBPF qui permettent d’observer toutes les fonctions (les symboles si on veut être précis) du noyau Linux ;
  • call permet de définir la fonction qu’on veut observer.

Nous allons ainsi voir un message à chaque fois que la fonction sys_openat est appelé dans le noyau Linux. Cette fonction est lancée à chaque fois que le syscall openat est démarré.

Vérifions :

kubectl apply -f tp-openat-basic.yaml

Relançons tetra de manière plus lisible :

kubectl exec -n kube-system ds/tetragon -c tetragon -- \
tetra getevents -o compact --pods nginx

Dans un autre terminal, connectons nous au pod nginx :

kubectl exec -it nginx -- bash

Que voit-on dans les logs Tetragon ?

🚀 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/bash

On voit qu’au démarrage de bash il lit pas mal de fichiers. Mais on ne voit pas lesquels.

Tracing policy un peu plus compliqué
#

Pour cela on va devoir modifier la tracing policy.

Regardons l’armure de la fonction sys_open_at() :

int sys_openat(int dirfd, const char *path, int flags, mode_t mode)

Il faut savoir lire une fonction en C :

  • int : correspond au code retour du syscall open_at => cela ne nous intéresse pas.
  • int dirfd : correspond à un descripteur de fichier. C’est un entier (int).
  • const char *path: correspond au chemin du fichier lu. Il est de type const char* ou string dans les langages de haut niveau

C’est exactement ce dernier paramètre qu’on veut avoir ! On va donc devoir rajouter les deux premiers arguments aux fichiers précédents.

Ainsi, on va utiliser args :

tp-openat-args.yaml
 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
  • index correspond à l’index de l’argument (on commence à 0 !)
  • type correspond au type de l’argument

On peut traduire par :

  • le premier argument est un descripteur de fichier, il est de type entier
  • le deuxième argument est un le chemin du fichier lu, il est de type string.

Vérifions :

kubectl replace -f tp-openat-args.yaml

Reconnectons nous au pod nginx, on sait maintenant exactement les fichiers qui sont lus :

🚀 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/inputrc

Vérifions maintenant qu’on a bien accès au fichier /var/run/secrets/kubernetes.io/serviceaccount/token dans le pod :

cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsImtpZCI6Ik9VRTBaVEFUTXNNaTRPOUZFTkZrbHRWVnZIQ0JfeUt1S0RRd2lLUnFtYncifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxODA4Mjc4OTA1LCJpYXQiOjE3NzY3NDI5MDUsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiMWI1ZGEwMjUtYzk2ZS00OTJjLWI2MTItZGE4YTUyY2ZmN2E1Iiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJkZWZhdWx0Iiwibm9kZSI6eyJuYW1lIjoia2luZC1jb250cm9sLXBsYW5lIiwidWlkIjoiZmNmYmU3M2EtYzhhMy00YzRmLWFiMjgtZWZhYTM4MDNiNTgwIn0sInBvZCI6eyJuYW1lIjoibmdpbngiLCJ1aWQiOiIxYWU2NmU4YS02MWFjLTQ5NmEtYWQ3My03OGNlNmNlN2JlNTgifSwic2VydmljZWFjY291bnQiOnsibmFtZSI6ImRlZmF1bHQiLCJ1aWQiOiIzNTAwZWY5NC00MWU4LTQ2YjctYmE4ZC1jMmVmZjdkZmY5YmQifSwid2FybmFmdGVyIjoxNzc2NzQ2NTEyfSwibmJmIjoxNzc2NzQyOTA1LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6ZGVmYXVsdDpkZWZhdWx0In0.JdkU6I6CJ3DCZDgC3iDfPy9MOC6Y60a5E0RKmkhJduC9j8hqmfR1wssESiYmrx_QRA5YhnOY6qARJZfIEKM9g_XQr_ZvI7MBjiRduqofROWZz5gMpFt_E1FBZTFbZsplc2QWuylAUozo0EUha56oFfM5TYimKPbJfOTywijHnbcZcXAvvZX9GLyAIu20rgzK9iIdqj1dSa89K48To_uGZIaaQUhvrF7dh4PSJ67Xuvp2qOA3Cq-xkdbxO-GKmAGasKfoJq5GCvAvQOxc6Rx2iXBbFKnPpTjMzvI0Xfx19_9e5I7l1XhYexVgkHBtPHBTri9eKMxVqEvRQm69g-TgLQ

Comment peut on l’en empêcher ?

Tracing policy final
#

Pour cela, il suffit de réutiliser la tracing policy précédente et envoyer un SIGKILL au processus quand le path correspond à la valeur souhaité :

tp-openat-enforcement.yaml
 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: Sigkill

Cette policy est relativement lisible : on sélectionne le deuxième argument qui correspond au chemin du fichier, s’il est égale à la valeur /var/run/secrets/kubernetes.io/serviceaccount/token, on lui envoie le signal Sigkill.

Vérifions :

kubectl replace -f tp-openat-enforcement.yaml
kubectl exec -it nginx -- bash
cat /var/run/secrets/kubernetes.io/serviceaccount/token

L’output est alors :

Killed

Au niveau de Tetragon, on 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 SIGKILL

C’est exactement ce qu’on voulait faire.

lab Cette tracing policy a pour but de montrer ce qu’on peut faire avec Tetragon. Cette policy est facilement contournable ! Petit exercice si vous voulez aller plus loin : Comment contourner cette policy ? comment la renforcer ?

Nous avons continué notre apprentissage de Tetragon :

  • l’installation sur un cluster Kubernetes ;
  • son fonctionnement sur Kubernetes ;
  • les logs en format json de Tetragon ;
  • une petite initiation au tracing policy.

Dans le prochain article, nous allons approfondir le dernier point.

Ma première fois avec Tetragon - Cet article fait partie d'une série.
Partie 2: Cet article

Articles connexes