Módulo 11 de 16

Módulo 11: Detección en Linux y en contenedores

Módulo 11: Detección en Linux y en contenedores

En Windows tienes un canal de eventos unificado y, encima, Sysmon. En Linux no hay nada parecido de serie: ni un bus de eventos estructurado activado por defecto, ni un agente equivalente a Sysmon instalado en la mayoría de las distribuciones. Lo que existe es un subsistema de auditoría en el propio kernel que no registra nada mientras no le des reglas explícitas, un puñado de demonios de log de aplicación que escriben texto libre, y un ecosistema de herramientas de terceros (algunas basadas en eBPF) que intentan cubrir el hueco. La telemetría de detección en Linux no viene puesta; se construye pieza a pieza, y cada pieza cubre una parte distinta del problema.

Este módulo recorre esas piezas por orden de aparición en un incidente típico: primero auditd, porque decide qué ve el kernel; después lo que Wazuh y las herramientas basadas en eBPF (con Falco como referencia) aportan por encima; después una serie de comportamientos concretos de Linux que un analista de SOC tiene que reconocer; después el registro de autenticación SSH; y por último qué cambia cuando el proceso investigado vive dentro de un contenedor o detrás de un servidor de API de Kubernetes.

Qué aprenderás

  • La sintaxis exacta de una regla de syscall de auditd (-a, -S, -F, -k) y cómo se aplica a execve, a escrituras en rutas sensibles y al abuso de binarios con setuid
  • Cómo leer lo que produce auditd con ausearch y aureport, y por qué una regla de execve sin acotar genera un volumen que entierra la señal real
  • Qué aportan el módulo de integridad de ficheros y el de evaluación de configuración del agente de Wazuh, y en qué se diferencian de una herramienta basada en eBPF como Falco
  • Qué necesita Falco para funcionar (driver de kernel, versión mínima, permisos) y cómo está construida una de sus reglas
  • Media docena de detecciones concretas de Linux: ejecución desde /tmp y desde directorios de servidor web, persistencia en cron y en systemd, modificación de authorized_keys, preload de bibliotecas y manipulación del histórico del shell
  • Qué diferencia un acceso SSH válido, uno fallido y uno por clave de uno por contraseña en el registro de autenticación, y cómo se separa la fuerza bruta de su versión lenta
  • Qué telemetría de un contenedor da el runtime y cuál da el kernel del host, y por qué el PID que ves desde fuera no es el mismo que ve el proceso desde dentro
  • Qué es el registro de auditoría del servidor de API de Kubernetes, qué preguntas responde y con qué backends se exporta

auditd, la pieza que hace de columna vertebral

auditd es el demonio de espacio de usuario que recibe, por un socket netlink, los eventos que genera el subsistema de auditoría del propio kernel. La parte importante es «del propio kernel»: las reglas no las evalúa auditd, las evalúa el kernel en cada llamada al sistema, y auditd solo recoge lo que ya salió filtrado y lo escribe en disco (por defecto en /var/log/audit/audit.log). Lo que no está en una regla, el kernel ni se molesta en generar, así que el coste en CPU depende de cuántas reglas tengas activas y de cuánto ruido produzca cada una, no de cuánta telemetría «podrías» pedir. Las reglas se cargan con auditctl (inmediato, no sobrevive a un reinicio) o con ficheros bajo /etc/audit/rules.d/ cargados con augenrules --load (persistentes); para este módulo basta con auditctl.

Cómo se escribe una regla de syscall

La sintaxis, documentada en las páginas de manual auditctl(8) y audit.rules(7), tiene cuatro piezas. -a lista,acción añade la regla a una lista (casi siempre exit, que evalúa la llamada al salir, cuando ya se sabe si tuvo éxito) con una acción (always, para que genere registro). -S indica la llamada al sistema por nombre o número, varias separadas por comas o en -S repetidos. -F campo=valor añade condiciones: arquitectura (arch=b64 o arch=b32, necesarias las dos porque un kernel de 64 bits sigue atendiendo procesos de 32 bits, cada uno con su propia tabla de syscalls), identidad (uid, euid, auid, gid), rutas (exe, path, dir) o resultado (success=1, exit=). Por último, -k añade una etiqueta de texto libre con la que buscar luego en los registros, de hasta 31 bytes según la propia página de manual.

auditctl -a always,exit -F arch=b64 -S execve -k exec_generic
auditctl -a always,exit -F arch=b32 -S execve -k exec_generic

Esas dos líneas registran, en las dos arquitecturas, todas las llamadas execve del sistema (es decir, cada vez que un proceso se convierte en otro programa) bajo la misma etiqueta. Es la regla más amplia de todas las de este módulo, y por eso es también la más ruidosa; se retoma en la sección sobre volumen.

Reglas para execve, escritura en rutas sensibles y setuid

Para vigilar la escritura en un fichero o directorio concreto existe una sintaxis distinta, la de las reglas de vigilancia (watch rules): -w ruta -p permisos -k etiqueta, donde -p acepta cualquier combinación de r (lectura), w (escritura), x (ejecución) y a (cambio de atributo). Para los ficheros que definen quién tiene acceso y con qué privilegios, una escritura fuera de una ventana de mantenimiento conocida merece revisión:

auditctl -w /etc/passwd -p wa -k cred_files
auditctl -w /etc/shadow -p wa -k cred_files
auditctl -w /etc/sudoers -p wa -k cred_files
auditctl -w /etc/ssh/sshd_config -p wa -k cred_files

Para el abuso de binarios con el bit setuid activado, la señal no es «se ejecutó un binario setuid» (pasa todo el rato con sudo, passwd o ping) sino que la identidad efectiva del proceso terminó siendo distinta de la real de quien lo lanzó. auditd tiene un tipo de condición para eso, la comparación entre dos campos con -C, que solo admite igual y distinto, y solo dentro del mismo grupo (uid, euid, suid, fsuid, auid entre sí; gid y variantes entre sí, sin mezclar):

auditctl -a always,exit -F arch=b64 -S execve -C uid!=euid -F euid=0 -k setuid_root_exec
auditctl -a always,exit -F arch=b32 -S execve -C uid!=euid -F euid=0 -k setuid_root_exec

Esta regla dispara cada vez que el identificador de usuario real difiere del efectivo y el efectivo termina siendo root, que es lo que ocurre tanto con un setuid root legítimo como, sin diferencia visible a este nivel, con uno mal configurado explotado. No distingue el uso legítimo del abuso; solo garantiza que el evento queda registrado para que lo distinga un analista.

Cómo se leen los registros: ausearch y aureport

Un evento de auditd no es una sola línea. Según describe la Guía de seguridad de Red Hat Enterprise Linux, un mismo execve produce varios registros que comparten número de serie: uno de tipo SYSCALL con el número de llamada, si tuvo éxito (success) y el código de salida (exit); uno de tipo CWD con el directorio de trabajo desde el que se lanzó; y uno o más de tipo PATH con cada ruta que se pasó como argumento. Leer eso a mano línea a línea no es viable ni siquiera en un laboratorio pequeño, así que el propio proyecto trae dos herramientas de consulta. ausearch busca eventos por criterios y los reagrupa por número de serie; sus opciones más usadas son -k (por etiqueta), -x (por ejecutable), -ts (desde una fecha u hora, admite today o recent), -sc (por syscall) y -i, que traduce uid/gid a nombres de cuenta. aureport produce resúmenes agregados en vez de eventos individuales: --summary para el total por tipo de informe, -x --summary por ejecutable, --key por etiqueta y --failed solo intentos fallidos.

ausearch -k exec_generic -ts today -i
ausearch -k cred_files -i
aureport -k --summary
aureport -x --summary

El problema real: volumen y reglas que se disparan con todo

La regla de execve genérica del principio de esta sección es también la que más rápido llena un disco. Cada script de mantenimiento, cada unidad de systemd que arranca un proceso corto, cada invocación de grep dentro de otro script, genera su propio execve. En un servidor con cron activo y un puñado de agentes de gestión, esa única regla puede producir, sin exagerar, la mayoría del volumen total de auditd. La configuración de referencia de Neo23x0 asume esto de forma explícita: mantiene execve entre la telemetría de alto valor aunque sepa que es de alto volumen, y deja la responsabilidad de acotarla al entorno donde se despliegue.

La guía de linux-audit.com sobre ajuste de rendimiento propone un método, no una lista de exclusiones fija: usar aureport para averiguar qué ejecutable o etiqueta genera el volumen antes de decidir si excluirlo, y colocar las reglas que más se disparan al principio, porque el kernel las evalúa en orden y una regla frecuente situada primero ahorra comparaciones. Dos controles adicionales, documentados en auditctl(8), protegen contra la pérdida de eventos cuando el volumen supera lo que el sistema puede procesar: auditctl -b 8192 fija el tamaño del búfer de eventos pendientes en el kernel (el valor por defecto documentado es 64), y auditctl -r 0 fija un límite de mensajes por segundo (0 significa sin límite). Ninguno de los dos reduce el ruido; solo evitan que auditd pierda eventos cuando lo hay.

Wazuh y las herramientas basadas en eBPF: lo que auditd no cubre por sí solo

El agente de Wazuh: integridad de ficheros y evaluación de configuración

Wazuh ya apareció en el curso de DFIR y en el laboratorio de este curso como plataforma de recolección; aquí interesan dos capacidades de su agente que auditd no ofrece. La primera es el módulo de integridad de ficheros (FIM), bajo el bloque <syscheck>: guarda la suma de comprobación y los atributos de cada fichero vigilado en un escaneo inicial, y en escaneos posteriores (programados, o en tiempo real vía inotify en Linux, según la documentación oficial de Wazuh) compara ese estado con el guardado y alerta si algo cambió. FIM no ve la llamada al sistema que modificó el fichero, ve el resultado; no sabe qué proceso lo tocó a menos que auditd (que Wazuh también puede integrar como fuente) se lo diga.

La segunda es la evaluación de configuración de seguridad (SCA), que compara el estado real de un sistema contra comprobaciones escritas en YAML, con baselines basadas en los benchmarks de CIS. Los benchmarks de CIS son gratuitos solo para uso no comercial, así que sus tablas de controles se citan y se explican en este curso, nunca se reproducen. SCA no es una vigilancia continua sino una fotografía periódica de cumplimiento; complementa a FIM y a auditd, no los sustituye.

Falco y la detección basada en eBPF

Falco resuelve el mismo problema que auditd (capturar syscalls del kernel y decidir cuáles importan) por un camino distinto: un driver captura el flujo completo de llamadas y un motor de reglas en espacio de usuario las evalúa en tiempo real contra un fichero YAML, en vez de que el kernel filtre según reglas cargadas de antemano. Cada regla combina una condición (una expresión booleana sobre campos del evento, como proc.name o container.id), macros (fragmentos de condición reutilizables) y listas (conjuntos de valores reutilizables). Falco es, desde el 29 de febrero de 2024, un proyecto graduado de la Cloud Native Computing Foundation con licencia Apache-2.0; la versión comprobada en esta sesión es la 0.44.1, del 11 de junio de 2026 según el registro de versiones del proyecto.

Para funcionar necesita uno de dos drivers, y la elección afecta directamente a los permisos. Según la documentación de fuentes de eventos del kernel, el módulo de kernel clásico exige privilegios completos y no puede ejecutarse con capacidades de Linux recortadas; la sonda eBPF moderna (la opción por defecto) solo necesita cuatro capacidades concretas (CAP_SYS_BPF, CAP_SYS_PERFMON, CAP_SYS_RESOURCE, CAP_SYS_PTRACE) y funciona desde el kernel 5.8 en adelante, salvo backports puntuales, porque necesita soporte de anillo de búfer BPF y de BTF.

Esta es la regla por defecto «Terminal shell in container», con el mismo contenido que tiene hoy en el repositorio de reglas del proyecto (la condición y la salida se muestran en una sola línea cada una, en vez del bloque YAML multilínea original, para que quepan aquí sin perder ni una palabra):

- rule: Terminal shell in container
  desc: A shell was used as the entrypoint/exec point into a container with an attached terminal.
  condition: spawned_process and container and shell_procs and proc.tty != 0 and container_entrypoint and not user_expected_terminal_shell_in_container_conditions
  output: A shell was spawned in a container with an attached terminal (evt_type=%evt.type user=%user.name user_uid=%user.uid user_loginuid=%user.loginuid process=%proc.name proc_exepath=%proc.exepath parent=%proc.pname command=%proc.cmdline terminal=%proc.tty exe_flags=%evt.arg.flags)
  priority: NOTICE
  tags: [maturity_stable, container, shell, mitre_execution, T1059]

(Regla original del repositorio falcosecurity/rules, fichero falco_rules.yaml, licencia Apache-2.0, copyright de The Falco Authors.) Nota el campo parent=%proc.pname: Falco lleva el árbol de procesos en cada evento, así que una regla puede condicionar sobre el nombre del proceso padre. auditd guarda el ppid numérico en cada SYSCALL pero no el nombre del ejecutable padre; cruzar ese ppid contra una tabla de procesos aparte es trabajo extra que Falco ya resuelve por diseño.

Detecciones concretas en Linux

Ejecución desde directorios de escritura temporal y desde el árbol del servidor web

Un atacante que consigue subir un fichero a un servidor Linux casi siempre lo deja donde puede escribir sin privilegios: /tmp (con permiso de escritura para todo el mundo por diseño) o el árbol del servidor web, si la aplicación tiene algún punto de subida vulnerable. Cómo se llega a subir ese fichero es explotación de la aplicación en sí, contenido del curso de Hacking Web; aquí solo interesa qué queda registrado cuando ya se ejecuta.

Con auditd, la vía práctica no es vigilar /tmp/* con una regla de vigilancia (las -w no admiten comodines, según confirma la base de conocimiento de Red Hat; solo aceptan una ruta exacta), sino registrar todos los execve con la regla genérica ya vista y filtrar después, en el SIEM, por el prefijo de la ruta del ejecutable. Es justo lo que hace esta regla real de SigmaHQ:

title: Program Executions in Suspicious Folders
id: a39d7fa7-3fbd-4dc2-97e1-d87f546b1bbc
status: test
description: Detects program executions in suspicious non-program folders related to malware or hacking activity
references:
    - Internal Research
author: Florian Roth (Nextron Systems)
date: 2018-01-23
modified: 2021-11-27
tags:
    - attack.t1587
    - attack.t1584
    - attack.resource-development
logsource:
    product: linux
    service: auditd
detection:
    selection:
        type: 'SYSCALL'
        exe|startswith:
            - '/tmp/'
            - '/var/www/'
            - '/home/*/public_html/'
            - '/usr/local/apache2/'
            - '/srv/www/'
            - '/usr/share/nginx/html/'
            - '/var/lib/mysql/'
            - '/var/named/'
            # lista truncada aqui: el original trae mas rutas de servidor web
    condition: selection
falsepositives:
    - Admin activity (especially in /tmp folders)
    - Crazy web applications
level: medium

(Regla original de SigmaHQ, ficha lnx_auditd_susp_exe_folders.yml, licencia DRL 1.1; se muestra un subconjunto de las dieciséis rutas reales, marcado en el propio bloque.) La lista completa cubre convenciones de varias distribuciones a la vez; en tu entorno solo aplica la que corresponda a tu instalación real.

La ejecución de un shell interactivo como hijo directo de un proceso de servidor web (nginx, apache2, php-fpm) es la firma más clásica de un webshell. Con auditd la aproximación habitual filtra por la identidad efectiva del proceso, no por el nombre del padre (que auditd no guarda), asumiendo que cualquier execve con el usuario del servidor web merece revisión:

title: Webshell Remote Command Execution
id: c0d3734d-330f-4a03-aae2-65dacc6a8222
status: test
description: Detects possible command execution by web application/web shell
author: Ilyas Ochkov, Beyu Denis, oscd.community
date: 2019-10-12
modified: 2025-12-05
tags:
    - attack.persistence
    - attack.t1505.003
logsource:
    product: linux
    service: auditd
    definition: |
        Required auditd configuration (b32/b64 pair repeats for execveat):
        -a always,exit -F arch=b64 -S execve -F euid=33 -k detect_execve_www
        Change the number "33" to the ID of your WebServer user. Default: www-data:x:33:33
detection:
    selection:
        type: 'SYSCALL'
        SYSCALL:
            - 'execve'
            - 'execveat'
        euid: 33
    condition: selection
falsepositives:
    - Admin activity
    - Crazy web applications
level: critical

(Regla original de SigmaHQ, ficha lnx_auditd_web_rce.yml.) El fichero fija el 33 como identificador de www-data por defecto en Debian y Ubuntu; en Red Hat o CentOS el usuario de Apache suele tener otro identificador, así que compruébalo con id www-data o id apache antes de desplegarla.

Persistencia en cron y en unidades de systemd (incluidos los temporizadores)

Cron sigue siendo el mecanismo de tareas programadas más viejo de Linux, y vive repartido en varios sitios: el crontab de cada usuario (bajo /var/spool/cron/crontabs/ en Debian y derivados), el fichero /etc/crontab del sistema, y los directorios /etc/cron.d/, /etc/cron.hourly/, /etc/cron.daily/, /etc/cron.weekly/ y /etc/cron.monthly/. Todos esos sitios son candidatos a una regla de vigilancia:

auditctl -w /etc/crontab -p wa -k cron_persist
auditctl -w /etc/cron.d/ -p wa -k cron_persist
auditctl -w /var/spool/cron/crontabs/ -p wa -k cron_persist

Existe también una vía que no pasa por tocar ningún fichero directamente: el propio binario crontab escribe su actividad al log de cron vía syslog, con la palabra REPLACE cada vez que alguien sustituye un crontab entero. SigmaHQ tiene una regla, Modifying Crontab (id af202fd3-7bff-4212-a25a-fb34606cfcbe, autor Pawel Mazur), que busca justo esa palabra en el logsource de servicio cron, sin depender de auditd en absoluto. Esta es la técnica T1053.003, Cron, que en la versión 19 de ATT&CK (la que fija este curso como referencia; ver la guía de MITRE ATT&CK del sitio) figura bajo las tácticas Execution, Persistence y Privilege Escalation.

Systemd ofrece un mecanismo equivalente y, según T1053.006, Systemd Timers, cada vez más usado como alternativa a cron: unidades .timer que disparan un servicio en un momento del calendario o tras un intervalo. Las rutas donde viven las unidades, tanto .service como .timer (el sistema no distingue por extensión, solo por ubicación), son /etc/systemd/system/ y /usr/lib/systemd/system/ a nivel de sistema, y ~/.config/systemd/user/ a nivel de usuario, esta última sin exigir privilegios de root. SigmaHQ mantiene sobre auditd la regla Systemd Service Creation (id 1bac86ba-41aa-4f62-9d6b-405eac99b485, autor Pawel Mazur, técnica T1543.002), que dispara con un registro PATH de tipo CREATE bajo esas rutas; como filtra por directorio y no por sufijo, un .timer también dispara la misma condición.

Modificación de authorized_keys

El fichero authorized_keys, bajo <directorio-home-del-usuario>/.ssh/authorized_keys, es la técnica T1098.004, SSH Authorized Keys (Persistence y Privilege Escalation): añadir ahí una clave pública propia permite volver a entrar sin contraseña ni sesión abierta. Se repite el problema de los comodines ya visto con /tmp: cada usuario tiene su propio fichero y una regla de vigilancia de auditd no admite -w /home/*/.ssh/authorized_keys, así que hay que enumerar cada ruta a mano:

auditctl -w /root/.ssh/authorized_keys -p wa -k ssh_authorized_keys_mod
auditctl -w /home/<usuario>/.ssh/authorized_keys -p wa -k ssh_authorized_keys_mod

Con pocas cuentas eso es asumible; con decenas de usuarios deja de serlo, y es justo el caso donde una herramienta de integridad de ficheros capaz de recorrer un árbol completo (el syscheck de Wazuh, apuntado a /home con monitorización recursiva) cubre mejor el hueco que una regla por ruta.

Preload de bibliotecas (LD_PRELOAD)

El cargador dinámico de Linux admite dos mecanismos para cargar una biblioteca antes que cualquier otra, y ambos están documentados en la página de manual ld.so(8): la variable de entorno LD_PRELOAD, que afecta solo al proceso que la tiene definida (y a los que arranque), y el fichero /etc/ld.so.preload, que según la misma página «tiene un efecto a nivel de todo el sistema, precargando las bibliotecas especificadas para todos los programas que se ejecuten en el sistema». Si se usan las dos vías a la vez, las bibliotecas de LD_PRELOAD se cargan primero. La técnica es T1574.006, Dynamic Linker Hijacking, con tácticas Stealth y Execution tras la reorganización de ATT&CK v19; la propia página de la técnica cita el malware Ebury enganchando funciones de libc como execve y readdir para ocultar sus propios artefactos de comandos como ls o netstat. SigmaHQ mantiene la regla Code Injection by ld.so Preload (id 7e3c4651-c347-40c4-b1d4-d48590fdf684, autor Christian Burkard de Nextron Systems, nivel high), tan simple como buscar la cadena /etc/ld.so.preload; su logsource no fija ningún servicio concreto, así que aplica sobre cualquier fuente que registre rutas de fichero o línea de comandos. Existe también una variante específica sobre registros PATH de auditd, lnx_auditd_ld_so_preload_mod.yml.

Manipulación del histórico del shell

Bash y zsh guardan cada comando que se teclea en un fichero (~/.bash_history o ~/.zsh_history por defecto, con la ubicación configurable vía la variable HISTFILE). Borrarlo o inutilizarlo es la técnica T1070.003, Clear Command History, clasificada bajo la táctica Stealth. SigmaHQ mantiene una regla por palabras clave que cubre buena parte de las formas conocidas de hacerlo:

title: Linux Command History Tampering
id: fdc88d25-96fb-4b7c-9633-c0e417fdbd4e
status: test
description: |
    Detects commands that try to clear or tamper with the Linux command history.
    This technique is used by threat actors in order to evade defenses and execute commands without them being recorded in files such as "bash_history" or "zsh_history".
author: Patrick Bareiss
date: 2019-03-24
modified: 2024-04-17
tags:
    - attack.stealth
    - attack.t1070.003
logsource:
    product: linux
detection:
    keywords:
        - 'cat /dev/null >*sh_history'
        - 'chattr +i*sh_history'
        - 'export HISTFILESIZE=0'
        - 'history -c'
        - 'history -w'
        - 'ln -sf /dev/null *sh_history'
        - 'rm *sh_history'
        - 'shred *sh_history'
        - 'truncate -s0 *sh_history'
        # lista truncada aqui: el original trae 15 patrones
    condition: keywords
falsepositives:
    - Unknown
level: high

(Regla original de SigmaHQ, ficha lnx_shell_clear_cmd_history.yml; se muestra un subconjunto de los patrones reales, marcado en el bloque.) La lista mezcla técnicas de gravedad distinta: history -c solo limpia la sesión en memoria, mientras que chattr +i sobre el fichero de historial lo marca inmutable a nivel de sistema de ficheros, así que cualquier escritura posterior falla en silencio. Ninguna dice «borré tu historial»; las dos dejan de escribir, justo lo que la regla intenta atrapar antes de que ocurra.

El registro de autenticación: qué queda de un acceso SSH

sshd escribe cada intento de autenticación al log del sistema, normalmente vía syslog en /var/log/auth.log (Debian) o /var/log/secure (Red Hat), o consultable con journalctl -u sshd en systemd; la ruta exacta varía, compruébala en tu distribución. El patrón documentado en varias fuentes coincidentes (no una captura real, sustituye cada marcador y verifícalo en tu sistema) tiene esta forma:

sshd[<pid>]: Accepted publickey for <usuario> from <IP> port <puerto> ssh2
sshd[<pid>]: Accepted password for <usuario> from <IP> port <puerto> ssh2
sshd[<pid>]: Failed password for <usuario> from <IP> port <puerto> ssh2
sshd[<pid>]: Failed password for invalid user <usuario> from <IP> port <puerto> ssh2

La diferencia entre las dos líneas de éxito está en el método: publickey significa que el cliente demostró posesión de la clave privada de una pública ya presente en authorized_keys, sin que la contraseña entrara en juego; password significa autenticación por contraseña, y su presencia en un servidor donde se supone acceso solo por clave es en sí misma una señal a revisar. La cuarta línea, con «invalid user», aparece cuando el usuario probado ni siquiera existe, habitual en escaneos automatizados con listas genéricas de nombres.

Detectar fuerza bruta es, en esencia, contar líneas de Failed password agrupadas por origen dentro de una ventana de tiempo, y la especificación de Sigma tiene un tipo de regla para eso, la correlación event_count. Este es el ejemplo real que trae la propia especificación:

title: Many failed logins
id: 0e95725d-7320-415d-80f7-004da920fc11
correlation:
    type: event_count
    rules:
        - 5638f7c0-ac70-491d-8465-2a65075e0d86
    group-by:
        - ComputerName
    timespan: 1h
    condition:
        gte: 100

(Ejemplo original de la especificación de reglas de correlación de Sigma.) Para SSH, el mismo mecanismo se adapta cambiando qué regla base cuenta (una que capture Failed password), agrupando por IP de origen en vez de por equipo, y ajustando ventana y umbral: la fuerza bruta clásica se ve como muchos intentos en pocos minutos desde el mismo origen, mientras que su versión lenta reparte los mismos intentos, o menos, a lo largo de horas o días, a veces rotando el origen para no cruzar el umbral que un analista revisaría a simple vista. Hace falta una segunda regla con una ventana mucho más amplia y un umbral proporcionalmente más bajo, asumiendo que también va a traer más falsos positivos por mirar más lejos en el tiempo.

Vale la pena remarcar qué NO ve auditd aquí: aceptar o rechazar una autenticación SSH lo decide sshd en espacio de usuario (con ayuda de PAM), no una syscall interceptable de forma directa; ese registro vive en el log de sshd, no en el de auditd. auditd entra en juego después, si PAM tiene pam_loginuid, etiquetando los procesos de esa sesión con el auid de quien inició sesión, el mismo campo de la regla de setuid de este módulo.

Contenedores: qué ve y qué no ve el host

Un proceso dentro de un contenedor sigue siendo, para el kernel del host, un proceso normal: mismo planificador, mismos cgroups, mismas llamadas al sistema. Un contenedor no es una máquina virtual con su propio kernel; es una combinación de espacios de nombres y de cgroups aplicada a procesos que el kernel trata igual que a cualquier otro. auditd, ejecutándose en el host, ve perfectamente los execve, las escrituras y las conexiones de un proceso «en» un contenedor. El problema no es de visibilidad; es de significado de lo que ve.

El primero de esos problemas es el PID. Según la página de manual pid_namespaces(7), «un proceso tiene un identificador de proceso en cada una de las capas de la jerarquía de espacios de nombres de PID en la que es visible». En la práctica: el mismo proceso que dentro del contenedor se ve a sí mismo como PID 1 tiene un número completamente distinto, y mucho más alto, visto desde el espacio de nombres inicial del host, que es justo el que usa auditd para etiquetar sus registros. Si cruzas un PID que auditd registró contra algo que la propia aplicación haya volcado a su log («proceso 1 hizo tal cosa»), los números no van a coincidir sin traducir entre espacios de nombres.

El segundo problema es de rutas: un contenedor monta su propio sistema de ficheros, así que /etc/passwd dentro del contenedor no tiene nada que ver con el del host, y auditd registra la ruta tal como la ve la syscall, reflejando el árbol del contenedor. El tercer problema es estructural: el subsistema de auditoría se diseñó para un único espacio de nombres global. Según recoge LWN.net, sobre un trabajo de años de parches todavía sin cerrar para dotarlo de identificadores de contenedor propios, auditd corre en el espacio de nombres inicial del host, y el espacio de nombres de red particiona los sockets netlink de forma que solo los procesos de ese espacio inicial hablan con el subsistema por la vía normal. No es algo pensado para levantarse «uno por contenedor»; es, por diseño actual del kernel, una capacidad de todo el host.

La forma práctica de atribuir un evento a su contenedor no pasa por un campo nativo de auditd, sino por correlacionar la ruta de cgroup del proceso (visible en /proc/<pid>/cgroup, que el runtime construye a partir del identificador que él mismo asignó) contra el inventario de ese runtime (docker ps, crictl ps). Falco hace justo esto de forma automática: extrae el identificador de contenedor del cgroup al procesar cada evento de creación de proceso y lo usa para enriquecer el evento con metadatos, sin que el analista tenga que correlacionar a mano; por eso la regla «Terminal shell in container» puede condicionar directamente sobre container como un campo más.

Aparte de lo que ve el kernel, el runtime aporta telemetría de ciclo de vida en vez de syscall. Docker emite, según su documentación oficial, eventos como create, start, die y destroy, y también exec_create, exec_start, exec_die y exec_detach para sesiones de docker exec: avisa de que alguien entró a un contenedor en marcha, sin el detalle de cada comando tecleado. containerd, la capa bajo Docker y bajo la mayoría de Kubernetes, emite sus propios eventos de tarea a nivel de shim (su paquete de eventos), con identificador de contenedor, de sesión de exec y PID.

La telemetría del runtime avisa de que alguien entró a un contenedor o lo manipuló desde fuera, y es barata de recoger; la del kernel (auditd o Falco) es la única que ve lo que pasa cuando el propio proceso dentro del contenedor arranca algo por su cuenta, tras explotar una vulnerabilidad de la aplicación, un caso que nunca pasa por el runtime porque nadie ejecutó docker exec. Cómo se responde ya confirmado el compromiso es terreno del curso de DFIR.

Kubernetes solo en lo que toca al SOC: el registro de auditoría del servidor de API

Todo lo anterior se queda en el nivel del nodo: qué proceso hizo qué dentro de un contenedor concreto. Por encima, en un clúster de Kubernetes, hay una capa distinta de preguntas: quién le pidió al clúster que hiciera algo (crear un pod, borrar un secreto, ejecutar un comando en un pod vía kubectl exec), sin importar todavía qué pasó después dentro. Esa capa la responde el registro de auditoría del servidor de API, kube-apiserver, el único punto por el que pasa cualquier operación contra el clúster.

Según la documentación oficial de Kubernetes, la auditoría está desactivada por defecto y se activa apuntando --audit-policy-file a una política que decide, regla por regla, con qué nivel se registra cada petición: None (no registrar), Metadata (usuario, marca de tiempo, recurso y verbo, sin cuerpo), Request (metadatos más cuerpo de la petición) o RequestResponse (petición y respuesta completas). Cada evento, por otro lado, puede quedar registrado en varias etapas: RequestReceived, ResponseStarted (peticiones de larga duración, como un watch), ResponseComplete y Panic. Este es el ejemplo de política que trae la propia documentación:

apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
  - "RequestReceived"
rules:
  - level: RequestResponse
    resources:
    - group: ""
      resources: ["pods"]
  - level: Metadata
    resources:
    - group: ""
      resources: ["pods/log", "pods/status"]
  - level: None
    resources:
    - group: ""
      resources: ["configmaps"]
      resourceNames: ["controller-leader"]
  - level: None
    users: ["system:kube-proxy"]
    verbs: ["watch"]
    resources:
    - group: ""
      resources: ["endpoints", "services"]
  - level: Metadata
    omitStages:
      - "RequestReceived"

La primera regla de esa política registra todo lo relacionado con pods (creación, borrado, y esa petición de exec tan relevante para un SOC) con el máximo detalle; la última actúa como cierre, capturando a nivel de metadatos cualquier cosa que no coincidiera antes.

El registro se exporta por uno de dos backends: el de log escribe a un fichero JSON Lines (--audit-log-path, con --audit-log-maxage/maxbackup/maxsize para la rotación), y el de webhook envía cada evento a una API remota, en modo batch por defecto, o blocking/blocking-strict si hace falta que un fallo de registro bloquee, o incluso tumbe, la propia petición.

Este registro responde quién pidió qué al plano de control, contra qué recurso, desde dónde y con qué resultado; una petición create contra pods/exec es cómo se ve un kubectl exec desde el lado de la API, complementando la telemetría de ciclo de vida del runtime ya vista. Lo que no responde es qué corre dentro de un pod una vez concedida la petición: sigue siendo terreno de auditd, Wazuh o Falco a nivel de nodo. El bastionado del propio clúster (RBAC, políticas de red, estándares de seguridad de pods) queda fuera de este curso.

Tabla resumen de fuentes de telemetría en Linux

Fuente Qué captura Qué no ves solo con ella Coste aproximado
auditd Syscalls exactas que pidas por regla (execve, aperturas, escrituras), a nivel de todo el host Nombre del proceso padre; a qué contenedor pertenece un evento; rutas con comodines Bajo si las reglas están acotadas; alto en volumen y disco con execve sin filtrar
Wazuh FIM (syscheck) Cambios (creación, modificación, borrado) en ficheros y directorios vigilados, con línea base de checksum La llamada al sistema que causó el cambio, ni qué proceso lo hizo, salvo integración con auditd Bajo; escaneos periódicos más tiempo real vía inotify en Linux
Wazuh SCA Conformidad de la configuración del sistema frente a baselines, muchas basadas en CIS Comportamiento en tiempo real; es una foto periódica, no una vigilancia continua Bajo; ejecuciones programadas
Falco (eBPF o módulo de kernel) Syscalls en tiempo real con contexto de proceso padre y de contenedor ya resuelto Nada que el kernel no exponga; necesita el driver cargado y permisos elevados Medio; evaluación de reglas sobre cada syscall capturada
Logs de aplicación (sshd, journald, syslog) Eventos que la propia aplicación decide registrar, como intentos de autenticación Cualquier syscall subyacente; depende por completo de que la aplicación registre el evento Muy bajo
Eventos del runtime de contenedores (Docker, containerd) Ciclo de vida del contenedor y sesiones de exec abiertas desde fuera Lo que ocurre dentro del contenedor si el proceso nunca pasó por el runtime Muy bajo
Registro de auditoría del servidor de API de Kubernetes Quién pidió qué al plano de control (crear, borrar, exec) y con qué resultado Todo lo que pasa dentro de un pod tras concederse la petición Bajo a medio, según el nivel elegido; RequestResponse es el más caro

Ejercicio de laboratorio

Necesitas una máquina Linux de laboratorio (una VM cualquiera con systemd y con el paquete auditd instalado y en marcha vale; puedes reutilizar la del módulo 2 de este curso) y acceso root para cargar reglas y ejecutar los comandos de prueba.

  1. Carga estas tres reglas, una por cada comportamiento que vas a generar (sustituye <usuario> por tu propio usuario de pruebas):
    auditctl -a always,exit -F arch=b64 -S execve -k exec_generic
    auditctl -a always,exit -F arch=b32 -S execve -k exec_generic
    auditctl -w /etc/crontab -p wa -k cron_persist
    auditctl -w /var/spool/cron/crontabs/ -p wa -k cron_persist
    auditctl -w /home/<usuario>/.ssh/authorized_keys -p wa -k ssh_authorized_keys_mod

    Confirma con auditctl -l que las cinco quedaron cargadas.

  2. Genera el primer comportamiento, ejecución desde un directorio de escritura temporal:
    mkdir -p /tmp/labtest
    printf '#!/bin/bashnecho hola desde el laboratorion' > /tmp/labtest/run.sh
    chmod +x /tmp/labtest/run.sh
    /tmp/labtest/run.sh

    Comprueba con ausearch -k exec_generic -ts recent -i que aparece el execve de run.sh.

  3. Genera el segundo comportamiento con el test #1 de Atomic Red Team para T1053.003 (licencia MIT), que copia tu crontab actual antes de sustituirlo:
    crontab -l > /tmp/notevil
    echo "* * * * * /tmp/evil.sh" > /tmp/persistevil && crontab /tmp/persistevil

    Comprueba ausearch -k cron_persist -i y si tu log de cron escribe «REPLACE», como busca la regla Sigma de este módulo. Deshaz el cambio con crontab /tmp/notevil.

  4. Genera el tercer comportamiento con el test #1 de Atomic Red Team para T1098.004, que reescribe authorized_keys con su propio contenido (seguro de repetir, no añade ninguna clave nueva):
    if [ -f ~/.ssh/authorized_keys ]; then ssh_authorized_keys=$(cat ~/.ssh/authorized_keys); echo "$ssh_authorized_keys" > ~/.ssh/authorized_keys; fi;

    Comprueba ausearch -k ssh_authorized_keys_mod -i.

  5. Deja el sistema con uso normal durante una hora sin generar más eventos de prueba. Pasada esa hora, ejecuta aureport -k --summary y anota cuántos eventos acumuló cada etiqueta. Es probable que exec_generic supere a las otras dos por uno o más órdenes de magnitud.
  6. Con ese dato delante, escribe dos o tres frases sobre qué harías con exec_generic en un servidor real: acotarla en el propio auditd (con qué filtro, dado que no admite comodines de ruta) o mantenerla amplia y mover el filtrado, por prefijo de ruta del ejecutable, a la capa de análisis, como sugiere la guía de linux-audit.com citada en este módulo.

Preguntas frecuentes

¿Por qué no basta con que auditd esté instalado y en marcha?

Porque auditd, por sí solo, apenas registra nada: el subsistema de auditoría del kernel solo genera eventos para las llamadas al sistema que una regla explícita le pida vigilar. Una instalación por defecto sin reglas cargadas no da visibilidad de detección; la telemetría útil sale de decidir, una por una, qué syscalls, qué rutas y con qué filtros interesan en ese sistema.

¿Falco sustituye a auditd, o se usan juntos?

Pueden convivir. auditd tiene sentido como capa de base en cualquier host Linux, incluidos los que no corren contenedores; Falco aporta valor sobre todo donde su contexto nativo de proceso padre y de contenedor importa, como nodos de Kubernetes. Ninguno decide por ti qué telemetría hace falta activar; ese diseño de detección sigue siendo tuyo, cambia solo el mecanismo de captura.

¿Por qué no se puede vigilar de un tirón /home/*/.ssh/authorized_keys con una sola regla de auditd?

Porque las reglas de vigilancia de auditd (las que usan -w) no admiten comodines ni patrones de ruta; cada una apunta a una ruta exacta y fija. Para cubrir el home de cada usuario hace falta una regla por usuario, o recurrir a una herramienta de integridad de ficheros capaz de recorrer un árbol completo, como el módulo de integridad de ficheros de Wazuh.

¿auditd, corriendo en el host, ve todo lo que pasa dentro de un contenedor?

Ve las llamadas al sistema, sí, porque el kernel es compartido. Lo que no hace de forma automática es decir a qué contenedor pertenece cada evento: registra el PID y las rutas tal como los ve el espacio de nombres inicial del host, que no coinciden con los que ve el propio proceso desde dentro. Atribuir el evento a un contenedor exige correlacionar por la ruta de cgroup contra el inventario del runtime, algo que herramientas como Falco ya hacen por diseño.

¿El registro de auditoría de Kubernetes sustituye a la telemetría de host dentro de los pods?

No. Cubre las peticiones que llegan al servidor de API (crear, borrar, ejecutar un comando en un pod), no lo que hace un proceso una vez corriendo dentro de un pod. Esa segunda parte sigue dependiendo de auditd, de Falco o del agente de Wazuh instalados en cada nodo, igual que en un host sin Kubernetes.

¿Cómo se distingue una fuerza bruta SSH normal de su versión lenta?

Con el mismo mecanismo de correlación por conteo de eventos (event_count de Sigma), con parámetros distintos. La clásica se ve como muchos intentos fallidos en poco tiempo desde el mismo origen, así que una ventana corta con umbral alto la atrapa. La lenta reparte los mismos intentos, o menos, a lo largo de horas o días, a veces rotando el origen para no cruzar ese umbral; hace falta una segunda regla con ventana más amplia y umbral proporcionalmente más bajo, asumiendo más falsos positivos por mirar más lejos en el tiempo.