Ir al contenido

eBPF para observabilidad del kernel Linux: Guía rápida

31 de julio de 2026 por
eBPF para observabilidad del kernel Linux: Guía rápida
Sergio Méndez

Hola a todos, quiero mostrarles cómo eBPF puede ayudarnos a entender qué está pasando dentro del kernel de Linux. En la práctica, es una de las formas más efectivas de incorporar observabilidad en un sistema, porque nos permite adjuntar pequeños programas a eventos del kernel y convertir comportamientos de bajo nivel en señales útiles.

La observabilidad es la capacidad de entender el estado interno de un sistema a partir de las señales que emite. Es importante porque los sistemas modernos son demasiado complejos como para razonar sobre ellos solo desde su comportamiento externo. En Linux, observabilidad significa recopilar trazas, logs, métricas y otros eventos para responder preguntas como: ¿qué pasó?, ¿por qué pasó? y ¿qué está pasando ahora?

Aquí es donde eBPF encaja muy bien. Nos da una forma segura y práctica de observar el comportamiento del kernel directamente, en el punto donde muchos de esos eventos realmente ocurren. Si eres nuevo en este tema, piensa en eBPF como una forma de colocar pequeños programas en puntos importantes del kernel de Linux e inspeccionar lo que ocurre allí. Los ejemplos de este post son prácticos y el repositorio completo está disponible aquí:

Nota: Las instrucciones paso a paso para ejecutar los ejemplos también están incluidas en los archivos README dentro de las carpetas del repositorio.

Qué aprenderás

  • Qué es eBPF
  • Cómo se trazan las llamadas al sistema
  • Cómo se cargan los programas eBPF
  • Ejemplos en Python y Go
  • Contenedores, Falco y Tetragon
  • Por qué eBPF es importante para la observabilidad

Entorno y preparación

Para estos ejemplos, asumiremos una máquina Linux con acceso root, headers del kernel y el toolchain necesario de eBPF. El objetivo es mantenerlo práctico y ejecutable. Si vienes del mundo Python, BCC es un punto de partida amigable porque te permite escribir y cargar programas eBPF desde Python sin tener que profundizar primero en herramientas de más bajo nivel.

En este repositorio, los ejemplos están organizados en tres áreas principales:

  • Ejemplos en Python con BCC.
  • Ejemplos en Go con Cilium eBPF.
  • Ejemplos de runtime con Falco y Tetragon.

El repositorio ya incluye la estructura necesaria para experimentar localmente en un entorno Linux.

Antes de ejecutar los ejemplos, es importante preparar correctamente el host. El repositorio incluye instrucciones de dependencias tanto para Ubuntu como para Alpine, así que puedes elegir la distribución que mejor se adapte a tu entorno.

Ubuntu

Instala los paquetes requeridos para BCC, bindings de Python y soporte de LLVM. LLVM es la toolchain que ayuda a convertir tu código eBPF en bytecode de bajo nivel que el kernel puede verificar y ejecutar, por lo que cumple un papel importante en el flujo aunque no lo escribas directamente:

sudo apt-get update
sudo apt-get install -y bpfcc-tools linux-headers-$(uname -r) python3-bpfcc
sudo apt-get install -y llvm clang
sudo apt-get install -y libbpf-dev libbpf

Alpine

Instala los paquetes requeridos para BCC, Python y desarrollo con eBPF:

apk update
apk add build-base linux-headers git unzip nano vim elfutils-dev
apk add bcc-tools python3 py3-pip py3-bcc
apk add linux-virt-dev
apk add llvm clang
apk add libbpf-dev libbpf

Si necesitas soporte para Go en Alpine, instala también:

apk add go

Puedes verificar que BCC está disponible con:

sudo python3 -c "from bcc import BPF; print('BCC installed correctly')"

Qué es eBPF

eBPF significa Extended Berkeley Packet Filter. Originalmente fue diseñado para filtrado de paquetes, y evolucionó hasta convertirse en una de las tecnologías más importantes en sistemas Linux modernos.

A alto nivel, eBPF permite ejecutar programas aislados (sandboxed) dentro del kernel de Linux. El desarrollador escribe código en un lenguaje restringido, la toolchain lo compila a bytecode eBPF, el kernel verifica ese bytecode y solo entonces se ejecuta en la máquina virtual eBPF cuando se dispara el hook correspondiente. Por eso eBPF suele describirse como un punto de extensión seguro y programable dentro del kernel.

El flujo, de forma aproximada, es:

Source code
   │
   ▼
eBPF bytecode
   │
   ▼
Kernel verification
   │
   ▼
Attach to hook
   │
   ▼
System call / kernel event
   │
   ▼
Linux kernel
   │
   ▼
eBPF virtual machine executes it
   │
   ├─ inspect context
   ├─ read data from maps
   └─ emit trace / alert / metric

El detalle importante es que eBPF no es solo código que se adjunta. Es un programa verificado y aislado que el kernel ejecuta únicamente cuando ocurre un evento relevante.

Los siguientes hooks se usan comúnmente:

  • kprobes, que se adjuntan a funciones del kernel y son una de las formas más simples de observarlas.
  • tracepoints, que se adjuntan a puntos de instrumentación estables.
  • hooks de red, usados para inspeccionar eventos relacionados con tráfico.
  • puntos de entrada y salida de syscalls, que permiten observar cómo los programas de espacio de usuario interactúan con el kernel.

El hook que adjuntas es el evento específico del kernel que quieres observar. En los ejemplos del repositorio, el hook es la ruta de entrada del syscall execve, por lo que el programa eBPF se ejecuta cada vez que se crea un nuevo proceso. En otras palabras, el hook es el lugar donde el kernel dice "este evento ocurrió", y ahí se adjunta el programa eBPF para inspeccionar o reaccionar.

La diferencia principal es la seguridad: un módulo de kernel puede hacer casi cualquier cosa, pero se ejecuta con privilegios completos y puede desestabilizar el sistema fácilmente si no está bien escrito. Un programa eBPF se verifica antes de que el kernel lo acepte, lo que lo hace mucho más seguro para instrumentación y observabilidad.

En otras palabras, eBPF te permite añadir comportamiento al kernel sin reemplazar el kernel.

Trazado de llamadas al sistema

Uno de los usos más comunes de eBPF es trazar llamadas al sistema. Una syscall es el límite entre el espacio de usuario y el espacio del kernel. Cuando un proceso llama a algo como execve, open, read o write, el kernel ejecuta el manejador correspondiente.

La relación se puede visualizar de forma simple:

User space program
        │
        ▼
   System call
        │
        ▼
 Linux kernel
        │
        ▼
  eBPF hook / kprobe
        │
        ▼
 eBPF program runs
        │
        ├─ inspect context
        ├─ read data from maps
        └─ emit trace / alert / metric

En la práctica, eBPF se adjunta al flujo de ejecución justo cuando el kernel está manejando una syscall o una llamada a función del kernel. Por eso puede observar el comportamiento con tanta precisión: se ejecuta exactamente donde ocurre el evento.

Con eBPF podemos adjuntar un programa en ese momento e inspeccionar qué está pasando. Por ejemplo, podemos observar cuándo un proceso inicia un nuevo programa, cuándo se escribe un archivo o cuándo se emite tráfico de red.

Nota: Si quieres experimentar con otras llamadas al sistema en tu propio código, la referencia de syscalls de Linux es un buen punto de partida: https://man7.org/linux/man-pages/man2/syscalls.2.html. Esa página lista muchas syscalls comunes como open, read, write, connect y bind, que puedes intercambiar usando el mismo patrón del ejemplo del repositorio.

El repositorio incluye un ejemplo en Python en code/python/exec.py que demuestra esta idea. Si lo ejecutas, adjunta una sonda en la ruta de execve e imprime un mensaje cada vez que un proceso inicia un nuevo programa. Es un ejemplo simple pero potente de cómo eBPF convierte un evento del kernel en algo visible y útil.

from bcc import BPF

program = r"""
int hello(void *ctx) {
    bpf_trace_printk("Hello World from eBPF!\n");
    return 0;
}
"""

b = BPF(text=program)
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")

print("Tracing execve()... Press Ctrl+C to stop.")
b.trace_print()

Este ejemplo es simple, pero muestra claramente la idea central: compila un pequeño programa estilo C a bytecode eBPF, lo carga en el kernel y lo adjunta a la ruta del syscall execve. En este caso, el hook es el punto de entrada de execve, lo que significa que el programa se ejecuta cada vez que se lanza un nuevo ejecutable. Cada vez que ocurre ese evento, el código eBPF se ejecuta en contexto de kernel y emite un mensaje de traza.

Una salida típica se ve así:

Tracing execve()... Press Ctrl+C to stop.
Hello World from eBPF!
Hello World from eBPF!
Hello World from eBPF!

Esa es la esencia de la observabilidad de bajo nivel: podemos observar el kernel en el punto donde el comportamiento cruza el límite entre espacio de usuario y espacio de kernel.

En este contexto, una traza es simplemente un registro de un evento en el momento en que ocurre dentro del kernel. Puede ser tan simple como una línea de log como "Hello World from eBPF!" o tan rica como un flujo estructurado de syscalls, nombres de procesos, rutas de archivos y datos de tiempo. El tracing es una forma práctica de recopilar esos registros, y está estrechamente relacionado con la observabilidad porque nos da la evidencia detallada necesaria para entender qué está haciendo el sistema. La idea importante es que la traza convierte un evento interno del kernel en algo observable y útil para un desarrollador u operador.

Carga de programas eBPF en el kernel

El flujo suele ser el siguiente:

  1. Un desarrollador escribe un pequeño programa en C, normalmente orientado a un hook específico.
  2. La toolchain compila ese programa a bytecode eBPF.
  3. El kernel verifica el bytecode.
  4. El programa se adjunta a un hook como kprobe o tracepoint.
  5. Cuando el hook se dispara, se ejecuta la lógica eBPF.

La parte importante es que el programa no se ejecuta como un proceso normal en espacio de usuario. Lo ejecuta el kernel cuando ocurre el evento relevante. Por eso eBPF es tan útil para seguridad, tracing y análisis de rendimiento.

Esta es una de las razones por las que eBPF suele describirse como una forma de añadir funcionalidad al kernel sin escribir un módulo completo: es más dinámico, más seguro y más fácil de cargar y descargar.

Un kprobe es uno de los tipos de hook más comunes. Nos permite adjuntar un programa eBPF al punto de entrada de una función del kernel, casi como un pequeño breakpoint para código del kernel. En términos prácticos, esto significa que podemos observar cuándo se llama a una función como sys_execve, inspeccionar el contexto y reaccionar sin modificar el código fuente del kernel. Eso es exactamente lo que usan los ejemplos de este repositorio para trazar ejecución de procesos y otros eventos de bajo nivel.

Ejemplos en Python y Go

Ejemplo en Python

Los ejemplos en Python de este repositorio muestran lo fácil que es comenzar con eBPF en un entorno amigable para scripting. El toolkit BCC lo hace accesible al permitir escribir pequeños programas y adjuntarlos a eventos del kernel desde Python.

Para ejecutar los ejemplos, asegúrate de tener el entorno preparado como se describió arriba y luego ejecuta un script desde la carpeta del repositorio. Por ejemplo, para ejecutar el ejemplo de exec:

cd code/python
sudo python3 exec.py

De forma similar, puedes ejecutar otros ejemplos como:

sudo python3 chmod.py
sudo python3 delete_file.py
sudo python3 ping.py
sudo python3 write_file.py

Otro ejemplo, code/python/ping.py, engancha una función de red y filtra tráfico por nombre de proceso:

from bcc import BPF

program = r"""
#include <uapi/linux/ptrace.h>
#include <linux/skbuff.h>

#define TASK_COMM_LEN 16

int kprobe____dev_queue_xmit(struct pt_regs *ctx, struct sk_buff *skb) {
    char comm[TASK_COMM_LEN];
    bpf_get_current_comm(&comm, sizeof(comm));

    if (comm[0] != 'p' || comm[1] != 'i' || comm[2] != 'n' || comm[3] != 'g' || comm[4] != '\0') {
        return 0;
    }

    u32 len = 0;
    bpf_probe_read_kernel(&len, sizeof(len), &skb->len);

    bpf_trace_printk("ping traffic pid=%d comm=%s len=%d\n",
                     bpf_get_current_pid_tgid() >> 32,
                     comm,
                     len);
    return 0;
}
"""

BPF(text=program).trace_print()

Este ejemplo muestra cómo eBPF puede observar comportamiento de red a muy bajo nivel. El programa se ejecuta en el kernel e inspecciona la estructura skb para paquetes salientes, pero solo imprime salida para el proceso ping. En otras palabras, te permite responder una pregunta muy concreta: ¿qué procesos están generando tráfico de red y qué tipo de paquetes salen del host? Una salida de ejemplo puede verse así:

ping traffic pid=1234 comm=ping len=64
ping traffic pid=1234 comm=ping len=64
ping traffic pid=1234 comm=ping len=64

El mismo repositorio también incluye otros ejemplos como monitoreo de permisos de archivos, detección de eliminación de archivos y seguimiento de escrituras, para que puedas combinarlos según lo que quieras observar.

Ejemplo en Go

El ejemplo en Go de este repositorio usa las librerías eBPF de Cilium y ofrece una experiencia ligeramente más orientada a producción. El programa en C de code/go/kprobe.c se adjunta a sys_execve e incrementa un contador almacenado en un mapa BPF:

Para compilar y ejecutarlo, usa el directorio del ejemplo en Go:

cd code/go
go generate
go build -o kprobe-example .
sudo ./kprobe-example

En otra terminal, dispara actividad como:

ls
whoami

Después deberías ver cómo el contador aumenta a medida que se invoca la función del kernel.

SEC("kprobe/sys_execve")
int kprobe_execve() {
        u32 key     = 0;
        u64 initval = 1, *valp;

        valp = bpf_map_lookup_elem(&kprobe_map, &key);
        if (!valp) {
                bpf_map_update_elem(&kprobe_map, &key, &initval, BPF_ANY);
                return 0;
        }
        __sync_fetch_and_add(valp, 1);

        return 0;
}

El cargador en Go en code/go/main.go carga el objeto, adjunta el programa a la función del kernel y lee el mapa periódicamente:

kp, err := link.Kprobe(fn, objs.KprobeExecve, nil)
if err != nil {
        log.Fatalf("opening kprobe: %s", err)
}

Este es un gran ejemplo de cómo eBPF puede usarse desde un lenguaje de sistemas moderno. En lugar de escribir un módulo de kernel completo, el programa se adjunta a un símbolo existente del kernel y recopila información en un mapa del kernel de alto rendimiento que el proceso en espacio de usuario puede leer. Una salida típica del ejemplo en ejecución se ve así:

Waiting for events..
sys_execve called 1 times
sys_execve called 2 times
sys_execve called 3 times

El mismo patrón puede extenderse a otros eventos del kernel, y justo por eso este repositorio es útil: te da un punto de partida para construir tu propia lógica de observabilidad.

Contenedores, Falco y Tetragon: eBPF en escenarios reales de runtime

Los ejemplos de runtime de este repositorio muestran cómo esta tecnología se vuelve importante en entornos de contenedores y seguridad. Una vez que viste los ejemplos pequeños, puedes pasar a herramientas de más alto nivel y hacer una pregunta más amplia: ¿cómo convierto eventos de bajo nivel del kernel en detección y alertas de runtime para cargas reales?

Falco

Falco es una herramienta open-source de seguridad en runtime que usa telemetría a nivel de kernel para detectar actividad sospechosa en tiempo real. Suele usarse para monitorear syscalls, acceso a archivos y comportamiento de contenedores, y puede generar alertas cuando una aplicación hace algo inesperado. El ejemplo en runtime/falco/README.md inicia Falco con acceso privilegiado para que pueda observar el host y correlacionar eventos con metadatos de contenedores.

Un ejemplo simple es leer un archivo sensible como /etc/shadow en el host. Falco puede alertar sobre ese comportamiento porque está observando eventos a muy bajo nivel.

El repositorio también incluye comandos concretos para ambos ejemplos de runtime. Para Falco, puedes iniciarlo con:

docker run --rm -it \
  --name falco \
  --privileged \
  -v /sys/kernel/tracing:/sys/kernel/tracing:ro \
  -v /var/run/docker.sock:/host/var/run/docker.sock \
  -v /proc:/host/proc:ro \
  -v /etc:/host/etc:ro \
  falcosecurity/falco:0.44.1

Después de ejecutarlo, puedes simular actividad sospechosa con:

sudo cat /etc/shadow

Una salida esperada de Falco (simplificada) se ve así:

15:22:41 Warning Sensitive file opened for reading
15:22:41    file=/etc/shadow proc=cat user=root container=host
15:22:41    rule=Read sensitive file (trusted dirs)

Los campos exactos y el texto pueden variar según el ruleset y la versión de Falco en uso.

Tetragon

Tetragon es una herramienta de seguridad y observabilidad del ecosistema Cilium que usa eBPF para inspeccionar eventos de Linux en forma orientada a políticas. Puede observar procesos, archivos y actividad de red, y ayudar a aplicar reglas de runtime sin requerir un módulo de kernel tradicional. El ejemplo en runtime/tetragon/README.md monta un archivo de política y usa Tetragon para capturar eventos relevantes. Este es un ejemplo claro de cómo eBPF soporta seguridad y observabilidad sin requerir una extensión tradicional del kernel.

Para Tetragon, el ejemplo inicia el runtime con un archivo de política montado en el contenedor:

docker run -d --name tetragon --rm --pull always \
  --pid=host --cgroupns=host --privileged \
  -v ${PWD}/file_monitoring.yaml:/etc/tetragon/tetragon.tp.d/file_monitoring.yaml \
  -v /sys/kernel/btf/vmlinux:/var/lib/tetragon/btf \
  quay.io/cilium/tetragon:v1.7.0

Luego puedes inspeccionar los eventos que recolecta con:

docker exec -ti tetragon tetra getevents -o compact

Una salida esperada de Tetragon (vista compacta, simplificada) puede verse así:

process_exec: binary=/usr/bin/cat args="cat /etc/shadow" pid=12345
process_kprobe: policy=file_monitoring event=security_file_open file=/etc/shadow

Los nombres de eventos y campos exactos pueden variar según la política montada y la versión de Tetragon.

Lo que estas herramientas tienen en común es que no necesitan reimplementar el kernel. Se apoyan en los propios puntos de instrumentación del kernel y usan eBPF para convertir esos eventos en señales estructuradas sobre las que desarrolladores y operadores pueden actuar.

Por qué eBPF es importante para la observabilidad

eBPF es potente porque nos da visibilidad del kernel al mismo nivel donde el sistema realmente se comporta. Nos ayuda a responder preguntas como:

  • ¿Qué procesos se están iniciando?
  • ¿A qué archivos se está accediendo?
  • ¿Qué tráfico de red está saliendo del host?
  • ¿Qué syscalls están involucradas en una secuencia sospechosa?
  • ¿Cómo podemos entender un problema de runtime sin modificar primero el código de la aplicación?

Por eso eBPF es tan atractivo para observabilidad, seguridad y tracing en runtime. Cierra la brecha entre las aplicaciones de espacio de usuario y el comportamiento subyacente del kernel que suele determinar rendimiento y corrección.

Siguientes pasos con Kubernetes

Una vez que te sientas cómodo con lo básico, el siguiente paso es explorar cómo se usa eBPF dentro de Kubernetes. En clústeres modernos, es una tecnología clave para redes, observabilidad y seguridad en runtime.

Proyectos como Cilium, Tetragon, Falco e Inspektor Gadget se apoyan en eBPF para ofrecer mayor visibilidad sobre contenedores y servicios, mejorar el manejo de tráfico y detectar comportamiento sospechoso a nivel de kernel.

El beneficio principal es simple: puedes entender qué está pasando en el clúster con mayor precisión, sin depender de enfoques más pesados o intrusivos.

Conclusión

eBPF se ha convertido en una de las formas más prácticas de observar e influir en lo que sucede dentro del kernel de Linux. Te permite adjuntar pequeños programas a eventos del kernel, recopilar información detallada y construir flujos de observabilidad y seguridad sin crear módulos tradicionales del kernel.

Su atractivo también es práctico: funciona desde distintos lenguajes como Python y Go, es relativamente sencillo para empezar y escala bien a entornos más grandes y complejos como sistemas basados en Kubernetes. En la práctica, eBPF es especialmente valioso para tracing, seguridad y observabilidad, incluyendo monitoreo de red, donde entender el comportamiento a nivel de kernel es esencial tanto para rendimiento como para protección.

Referencias

Módulos del Kernel Linux que explican cómo Podman funciona