Guilgo Blog

Notas de mi quehacer diario con las técnologias.

Wazuh ya trae un módulo nativo para el ciclo de vida de Docker. El atajo habitual es copiar un post, pegar reglas en local_rules.xml y seguir. En 4.14.7 ese atajo sobra: el ruleset oficial ya usa docker.Action y cubre create, start, kill, die, destroy, pull y exec. Lo que sí hay que decidir es quién habla con /var/run/docker.sock.

En corto: en Wazuh 4.14.7, docker-listener funciona con el ruleset de fábrica. Actívalo en el agente del host que ya habla con Docker; no expongas /var/run/docker.sock al contenedor del manager.

Este post documenta cómo lo activamos en el homelab: manager Wazuh 4.14.7 en Docker, agente nativo en el host raam (el que también corre servicios como contenedores Docker directos; no todo pasa por k3s). Cero XML custom. El agente 008 se suscribe al stream de eventos de Docker y el manager alerta con las reglas de fábrica.

Versión en inglés: Monitor Docker containers with Wazuh docker-listener: no custom rules needed.

La inspiración (y el disparador) es el artículo de Saif Ullah (@saifsocx) en Medium: Docker Monitoring with Wazuh (agosto 2026). Advertía de un desajuste en las reglas por defecto (esperaban docker.status; el listener manda docker.Action) que obligaba a sobreescribir XML a mano. En 4.14.7 ese bug no existe. Merece la pena mirar el ruleset de tu versión antes de copiar un workaround pensado para otra.

La referencia oficial sigue siendo Using Wazuh to monitor Docker. Encaja con lo ya montado de auditoría de Kubernetes (otra superficie: el API server, no el daemon de Docker) y el dashboard Grafana sin Elasticsearch.

Qué cambia respecto al artículo de Saif Ullah

En el artículo de Medium En este homelab (Wazuh 4.14.7)
Reglas custom en local_rules.xml porque el ruleset buscaba docker.status El ruleset oficial ya usa docker.Action; cero XML custom
interval descrito como polling de eventos Stream en tiempo real (client.events()); interval solo relanza el proceso si se cae
Manager y agente en VMs Ubuntu + Dashboard oficial Manager en Docker, agente nativo en el host, Grafana + alerts.json

Dónde vive el acceso al socket

Hay dos maneras de dar al wodle acceso a /var/run/docker.sock:

  1. Agente nativo en el host (la que usamos). El agente ya corre como proceso del sistema; basta meter a su usuario (wazuh) en el grupo docker.
  2. Montar el socket dentro del contenedor del manager y activar el wodle ahí. El manager también actúa como su propio agente local, así que “funciona” en un docker compose y no hace falta sudo en el host.

La opción 2 es más rápida de montar. También le da al contenedor del manager control root-equivalente sobre el daemon Docker del host. Si ese contenedor ejecuta integraciones que procesan datos externos (reputación de URLs, bots de Telegram), estás ampliando la superficie de un componente que ya habla con el exterior.

Docker sirve para empaquetar y aislar procesos. El fallo no es “usar contenedores”; es tratar el aislamiento operativo como frontera fuerte y, encima, regalar el socket a un proceso que no lo necesita. En este blog ya se habló de ese modelo de amenaza en Docker nunca fue un sandbox. Aquí la consecuencia práctica es simple: el listener vive donde ya vivía el privilegio (el agente del host), no se lo damos al manager.

Host (raam)
 ├─ dockerd  (/var/run/docker.sock, grupo docker)
 ├─ wazuh-agent  (systemd, agente 008, usuario wazuh ∈ docker)
 │    └─ wodle docker-listener  →  stream de eventos Docker
 │
 └─ contenedor wazuh-lite  (manager 4.14.7)
      ├─ recibe el agente 008 por 1514/tcp
      ├─ ruleset 0560-docker_integration_rules.xml  (87900–87958)
      └─ alerts.json / Grafana

El punto clave: el agente que vigila Docker corre en el host, no dentro del contenedor del manager.

Activar docker-listener en el agente

En el host que corre dockerd, no en el manager:

sudo apt install -y python3-pip
sudo pip3 install docker==7.1.0 urllib3==1.26.20 requests==2.32.2

Esas versiones son las que documenta Wazuh. En Python 3.11+ puede hacer falta --break-system-packages (PEP 668); si tu distro ya gestiona el entorno, un venv y cambiar el shebang de /var/ossec/wodles/docker/DockerListener evita tocar el Python del sistema.

sudo usermod -aG docker wazuh
groups wazuh   # debe listar docker

Nota de seguridad: estar en el grupo docker es, en la práctica, equivalente a root en el host. El punto no es eliminar ese privilegio, sino dejarlo en el componente que de verdad lo necesita.

En /var/ossec/etc/ossec.conf, antes de </ossec_config>:

<wodle name="docker-listener">
  <disabled>no</disabled>
  <interval>1m</interval>
  <attempts>5</attempts>
  <run_on_start>yes</run_on_start>
</wodle>

interval no es un polling de eventos Docker. En 4.14.7 el wodle Python se queda en client.events() (el stream de eventos). interval es la espera de wazuh-modulesd antes de relanzar ese proceso si se cae; attempts limita los reintentos.

sudo /var/ossec/bin/wazuh-agentd -t
sudo systemctl restart wazuh-agent

Nada de esto toca el manager: ni su ossec.conf ni local_rules.xml.

El ruleset oficial ya es suficiente

Comprobado dentro del contenedor del manager:

docker exec wazuh-lite cat /var/ossec/ruleset/rules/0560-docker_integration_rules.xml

En Wazuh 4.14.7, las reglas Docker de fábrica son los IDs 87900–87958, grupo docker, anclados en docker.Action (o docker.Type para red, volumen y servicio). Cobertura real: contenedores (create, start, stop, pause, kill, die, destroy, rename, exec), imágenes (pull, push, tag, commit), volúmenes, redes, secrets, plugins, nodos y servicios. MITRE ATT&CK y mapeos de compliance (PCI-DSS, HIPAA, NIST, TSC) ya vienen de fábrica.

No hizo falta escribir ni una regla en local_rules.xml.

Verificación: un ciclo de vida real

Generamos docker rundocker stopdocker rm con una imagen ya local (sin depender de Docker Hub) y miramos alerts.json en el manager:

Acción real Regla Nivel Qué ves
create 87901 3 Container … created
start 87903 3 Container … started
kill (SIGTERM, del docker stop) 87924 7 received the action: kill
kill (SIGKILL, tras el timeout de gracia) 87924 7 received the action: kill
stop 87904 3 Container … stopped
die 87924 7 received the action: die (exitCode 137)
destroy 87902 5 Container … destroyed

137 es 128+9: el contenedor murió por SIGKILL. La regla 87924 cubre tanto kill como die; por eso aparecen varias alertas de nivel 7 en un docker stop normal. Eso importa más adelante.

Ejemplo real (alerts.json):

{
  "timestamp": "2026-08-28T15:46:52.044+0000",
  "rule": {
    "level": 7,
    "description": "Docker: Container wazuh-listener-test received the action: die",
    "id": "87924",
    "groups": ["docker"],
    "gdpr": ["IV_32.2"]
  },
  "agent": { "id": "008", "name": "raam", "ip": "127.0.0.1" },
  "decoder": { "name": "json" },
  "data": {
    "integration": "docker",
    "docker": {
      "Type": "container",
      "Action": "die",
      "Actor": { "Attributes": { "exitCode": "137", "name": "wazuh-listener-test" } }
    }
  }
}

Todo llegó etiquetado con agent.id: 008 y groups: ["docker"], sin regla custom de por medio.

Por qué las alertas de Docker no van a Telegram todavía

La integración custom-telegram.py filtra por level >= 7 y por grupo (control_parental, adguard, hayabusa, aur_malware, urlscan). El grupo docker no está en esa lista. Hoy estas alertas viven en el dashboard y en alerts.json; no pegan en el móvil, ni siquiera las de nivel 7 (kill / die).

Es deliberado. En este host hay redeploys frecuentes con docker compose up -d --build. Cada redeploy legítimo genera stop / kill / die / destroy y dispararía nivel ≥ 7. Meter docker entero en el filtro de Telegram es convertir un redeploy de n8n en una ráfaga al teléfono.

Antes de conectar el grupo conviene mirar unos días el volumen real en Grafana y, casi seguro, filtrar por acciones concretas (un pull de una imagen que no reconoces, un exec_start: bash en un contenedor de producción) en vez de por el grupo entero. El mismo criterio de bajo ruido que en el SOC proactivo del homelab y en las alertas de urlscan: señal, no teatro.

Qué cubre (y qué no)

  • El wodle solo ve el daemon del host donde corre el agente. No cubre contenedores de k3s. Para el plano de control ya está la auditoría de Kubernetes vía webhook.
  • Sin Telegram, la visibilidad es pull (dashboard), no push. Hay que entrar a mirar.
  • No lee logs de la aplicación dentro del contenedor. Eso sigue siendo montar el fichero en el host y un <localfile>, no este wodle.

Para desactivar: quita (o pon <disabled>yes</disabled> en) el bloque del wodle y systemctl restart wazuh-agent.

Conclusión

No siempre hay que reescribir reglas para adoptar una integración “nueva” de Wazuh. Primero mira qué trae el ruleset oficial de tu versión. Y cuando la integración necesita un socket con privilegio real, la pregunta de quién tiene ese acceso importa tanto como el “¿funciona?”.

Lo que se gana: visibilidad del ciclo de vida de los contenedores que corren fuera de k3s, cero mantenimiento de XML custom, y el socket de Docker sin exponerse al manager de Wazuh.