Tetragon es un proyecto de Cilium que engancha funciones internas del kernel de Linux con eBPF y decide en el mismo instante si algo se permite o se mata.
Aquí lo instalas, ves qué son una función del kernel, un kprobe y el ring buffer, y escribes dos TracingPolicy: una que protege tu clave privada de SSH y otra que mata una reverse shell antes de que logre salir.

Dos formas de vigilar el kernel
Tetragon engancha funciones internas del kernel de Linux con eBPF y decide en el mismo instante si algo se permite o se mata. ¿Y cómo lo hace? Eso es lo que vamos a ver.
Hay dos formas de vigilar un kernel. La primera es con un programa fuera, en el espacio de usuario. En ese caso, el programa primero tiene que recibir el aviso, después leerlo, y solo entonces decidir si manda frenar algo. En resumen, es un viaje de ida y vuelta.
La otra forma es tener un componente mirando directamente dentro, en el kernel space. Ese componente decide y actúa en el mismo instante, sin salir a preguntarle a nadie. Y eso es exactamente lo que hace Tetragon.
Vamos a hacer dos demos: primero proteger un archivo concreto, y después matar una reverse shell antes de que logre salir.
Instalar Tetragon y ver los primeros eventos
Primero instalo el agente y la CLI:
curl -LO https://github.com/cilium/tetragon/releases/download/v1.7.0/tetragon-v1.7.0-arm64.tar.gz
tar -xzf tetragon-v1.7.0-arm64.tar.gz
cd tetragon-v1.7.0-arm64 && sudo ./install.sh
sudo systemctl restart tetragon
curl -L https://github.com/cilium/tetragon/releases/download/v1.7.0/tetra-linux-arm64.tar.gz | tar -xz
sudo mv tetra /usr/local/bin/
El agente queda corriendo como servicio con systemd, y la herramienta de línea de comandos se llama tetra.
Ahora abro la escucha de eventos, todavía sin ninguna política:
sudo tetra getevents -o compact
Abro otra terminal y entro por SSH. En el instante en que me autentico, la primera terminal empieza a mostrar eventos: todo lo que acaba de pasar durante el login aparece en orden, sin que yo haya configurado nada.
Lanzo un par de comandos rápidos:
ls /tmp
whoami
En la ventana de eventos aparecen tanto ls como whoami, cada uno con su inicio y su salida.
Tetragon ya está viendo todo lo que pasa en el sistema. Lo interesante viene cuando le pedimos que, además de mirar, actúe. Para entender cómo lo hace, necesitas tres conceptos que vamos a usar durante el resto de la lección.
Qué necesitas saber antes de escribir una política
Qué es una función del kernel
El kernel de Linux es un programa en C de millones de líneas de código, organizado en miles de funciones, y cada una hace una sola cosa. Hay una función que se ejecuta cuando abres un archivo, y otra completamente distinta cuando arrancas una conexión de red.
El propio kernel tiene una lista con el nombre y la dirección de cada función, y la puedes ver con este comando:
cat /proc/kallsyms
Esa lista es, literalmente, el menú del que elige Tetragon.
Qué es un kprobe
Un kprobe es un mecanismo que ya trae el kernel de serie, pensado originalmente para depuración. Te deja elegir cualquier función de esa lista y decirle al kernel: «Cuando llegues aquí, primero ejecuta este otro código mío». No hace falta recompilar nada, y funciona en caliente, mientras el sistema sigue corriendo.
En el caso de Tetragon, ese código es eBPF: inspecciona los argumentos de la función, toma una decisión, y después la deja seguir su curso o la corta ahí mismo.
Cómo se comunican el kernel y el agente: el ring buffer
El agente de Tetragon corre fuera, en el espacio de usuario, y los programas eBPF corren dentro del kernel. Para que los eventos le lleguen al agente sin perder tiempo, los dos comparten directamente una región de memoria, sin una llamada al sistema por cada evento. Las llamadas al sistema (syscalls) las vimos en la lección del kernel.
Esa región de memoria compartida se llama ring buffer, y le dedicaré una lección aparte.
Primera política: proteger tu clave privada de SSH
Con estos tres conceptos claros, ya podemos escribir la primera política. El archivo se llama TracingPolicy, y en él le dices a Tetragon qué función enganchar, qué condición evaluar y qué hacer si se cumple.
Vamos a ponernos en una situación real: imagina que alguien logra ejecutar código en esta máquina y ese proceso intenta leer tu clave privada de SSH para robarla.
Para evitarlo, engancho una función del kernel que se llama security_file_permission. Es un punto de control que ya viene de serie: justo antes de dejar que alguien lea un archivo, evalúa si lo permite o si lo corta en el acto.
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: "proteger-ssh"
spec:
kprobes:
- call: "security_file_permission"
syscall: false
args:
- index: 0
type: "file"
- index: 1
type: "int"
selectors:
- matchArgs:
- index: 0
operator: "Postfix"
values: ["id_ed25519"]
matchActions:
- action: Sigkill
Básicamente le digo: vigila si el archivo termina en id_ed25519 y, si coincide, mata el proceso de inmediato.
Aplico la política:
sudo tetra tracingpolicy add proteger-ssh.yaml
Y ahora simulo ser ese proceso atacante intentando leer la clave:
cat ~/.ssh/id_ed25519
La terminal solo muestra Killed. Ni una sola línea de la clave llega a salir por pantalla.
En la ventana de eventos aparece reflejado: un evento process_kprobe, con la acción de matar registrada en tiempo real.
Segunda política: matar una reverse shell
Esto resuelve el caso de proteger un archivo concreto. ¿Pero qué pasa si el atacante no se conforma con eso y quiere tomar el control remoto del servidor?
Para eso se usa una reverse shell. Imagina que tu aplicación tiene una vulnerabilidad, alguien la explota y consigue que tu propio proceso abra una conexión saliente hacia su máquina. En ese momento, le acabas de entregar una terminal completa.
Vamos a cortarlo antes de que esa conexión termine de abrirse. Para eso enganchamos otra función, tcp_connect, que se ejecuta en el instante exacto en el que arranca cualquier conexión TCP saliente, antes de enviar el primer paquete.
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: "kill-reverse-shells"
spec:
kprobes:
- call: "tcp_connect"
syscall: false
selectors:
- matchBinaries:
- operator: "In"
values: ["/bin/bash", "/usr/bin/bash", "/bin/sh", "/usr/bin/sh"]
matchActions:
- action: Sigkill
La lógica es esta: si el binario que intenta abrir la conexión es una shell como bash o sh, lo matamos.
Aplico la política y simulo el ataque intentando conectar hacia fuera por el puerto 80:
sudo tetra tracingpolicy add kill-reverse-shells.yaml
bash -c 'exec 3<>/dev/tcp/example.com/80; echo hola >&3'
El resultado es Killed. al instante.
Pero fíjate en este detalle, porque no estamos bloqueando la red entera:
curl -sI https://pypi.org
Funciona perfectamente, sin ningún corte. ¿Por qué? Porque curl es un binario legítimo que no está en nuestra lista, así que Tetragon lo deja pasar aunque también se conecte a internet.
Lo que bloqueamos no es la red, sino un comportamiento sospechoso, porque una shell abriendo sockets por su cuenta es, casi siempre, un ataque.
Afinar la política para que solo actúe en contenedores
Digo casi siempre porque, si algún día necesitas hacer pruebas en tu propio host, esta política también te mataría a ti. Así que vamos a afinarla para que solo actúe dentro de contenedores y no en el host.
Le añado una condición clave a la política: filtrar por namespaces, para que ignore el namespace del host.
matchNamespaces:
- namespace: "Pid"
operator: "NotIn"
values: ["host_ns"]
Vuelvo a aplicar la política y pruebo los dos escenarios. Primero, en mi propia terminal:
timeout 5 bash -c 'exec 3<>/dev/tcp/example.com/80; echo hola >&3; echo "sigo vivo"'
Termina sin problemas y sale el mensaje «sigo vivo». En el host ya no nos bloquea.
Ahora viene la prueba importante: exactamente el mismo comando, pero dentro de un contenedor Docker:
sudo docker run --rm ubuntu bash -c 'exec 3<>/dev/tcp/example.com/80; echo hola >&3'
Esta vez no imprime nada en pantalla. En la ventana de eventos aparece exit 137, que es el mismo Killed que vimos antes. Docker no te muestra el mensaje de texto, pero el proceso murió igual y el código de salida lo confirma.
Lo que hemos visto
Esta es la tercera lección de la serie eBPF, Cilium y Tetragon. Si todavía no has visto la lección de eBPF, es la base de todo lo que hemos hecho aquí.
- Tetragon decide dentro del kernel, en el mismo instante, si una acción se permite o se mata, sin ir y volver al espacio de usuario.
- Un kprobe te deja ejecutar tu propio código eBPF cada vez que se llama a una función del kernel, y el agente recibe los eventos por un ring buffer.
- Con una TracingPolicy sobre
security_file_permission, cualquier proceso que intenta leerid_ed25519muere conSigkill. - Con
tcp_connectymatchBinariesmatas las shells que abren conexiones salientes, sin bloquear binarios legítimos comocurl. - Con
matchNamespaceshaces que la política ignore el host y solo actúe dentro de los contenedores.