Wazuh already ships a native module for the Docker container lifecycle. The usual shortcut is to copy a blog post, paste rules into local_rules.xml, and move on. On 4.14.7 that shortcut is leftover advice: the official ruleset already keys off docker.Action and covers create, start, kill, die, destroy, pull and exec. The decision that still matters is who talks to /var/run/docker.sock.
TL;DR: On Wazuh 4.14.7, docker-listener works with the stock ruleset. Run it on the host agent that already has access to Docker instead of exposing /var/run/docker.sock to the manager container.
This is how we turned it on in the homelab: Wazuh manager 4.14.7 in Docker, native agent on the host raam (the same box that also runs services as plain Docker containers; not everything sits in k3s). Zero custom XML. Agent 008 subscribes to the Docker event stream; the manager alerts with stock rules.
Spanish write-up: Monitorizar contenedores Docker con Wazuh docker-listener.
The starting point was Saif Ullah’s (@saifsocx) Medium article Docker Monitoring with Wazuh (August 2026). It warned that default rules expected docker.status while the listener emits docker.Action, so you had to override XML by hand. That mismatch is gone in 4.14.7. Check the ruleset that actually shipped with your version before you copy a workaround written for a different one.
Official reference: Using Wazuh to monitor Docker. It sits next to Kubernetes audit logging (API server, not the Docker daemon) and the Grafana dashboard without Elasticsearch.
What changed versus Saif Ullah’s article
| In the Medium article | In this homelab (Wazuh 4.14.7) |
|---|---|
Custom rules in local_rules.xml because the stock ruleset looked for docker.status |
Official ruleset already keys off docker.Action; zero custom XML |
interval described as polling Docker events |
Real-time stream (client.events()); interval only relaunches the process if it exits |
| Manager and agent on Ubuntu VMs + official Dashboard | Manager in Docker, native host agent, Grafana + alerts.json |
Who gets the socket
Two ways to give the wodle /var/run/docker.sock:
- Native agent on the host (what we used). The agent is already an OS process; add its user (
wazuh) to thedockergroup. - Bind-mount the socket into the manager container and enable the wodle there. The manager also acts as its own local agent, so it “just works” from
docker composewith no sudo on the host.
Option 2 is quicker to set up. It also hands the manager container root-equivalent control of the host Docker daemon. If that container runs integrations that process untrusted input (URL reputation, Telegram bots), you have widened the attack surface of a component that already talks to the outside world.
Containers are a legitimate way to package and isolate processes. The failure mode is treating that operational isolation as a hard security boundary, then giving away the socket to a process that does not need it. Same threat model as Docker was never a sandbox. Practical consequence: the listener stays where the privilege already lived (the host agent). The manager does not get a new trust relationship.
Host (raam)
├─ dockerd (/var/run/docker.sock, docker group)
├─ wazuh-agent (systemd, agent 008, user wazuh ∈ docker)
│ └─ wodle docker-listener → Docker event stream
│
└─ wazuh-lite container (manager 4.14.7)
├─ receives agent 008 on 1514/tcp
├─ ruleset 0560-docker_integration_rules.xml (87900–87958)
└─ alerts.json / Grafana
The agent watching Docker runs on the host, not inside the manager container.
Enable docker-listener on the agent
On the host that runs dockerd, not on the manager:
sudo apt install -y python3-pip
sudo pip3 install docker==7.1.0 urllib3==1.26.20 requests==2.32.2
Those pins are the ones Wazuh documents. Python 3.11+ may need --break-system-packages (PEP 668). A venv plus a shebang change on /var/ossec/wodles/docker/DockerListener avoids touching the distro Python.
sudo usermod -aG docker wazuh
groups wazuh # must list docker
Security note: membership in the docker group is effectively root-equivalent on the host. The point here is not to eliminate that privilege, but to keep it on the component that actually needs it.
In /var/ossec/etc/ossec.conf, before </ossec_config>:
<wodle name="docker-listener">
<disabled>no</disabled>
<interval>1m</interval>
<attempts>5</attempts>
<run_on_start>yes</run_on_start>
</wodle>
interval is not a poll of Docker events. In 4.14.7 the Python wodle sits on client.events() (the Docker event stream). interval is how long wazuh-modulesd waits before relaunching that process after it exits; attempts caps the retries.
sudo /var/ossec/bin/wazuh-agentd -t
sudo systemctl restart wazuh-agent
None of this touches the manager: not its ossec.conf, not local_rules.xml.
Stock ruleset is enough
Checked inside the manager container:
docker exec wazuh-lite cat /var/ossec/ruleset/rules/0560-docker_integration_rules.xml
In Wazuh 4.14.7, the stock Docker rules are IDs 87900–87958, group docker, keyed on docker.Action (or docker.Type for network, volume and service events). Coverage: containers (create, start, stop, pause, kill, die, destroy, rename, exec), images (pull, push, tag, commit), volumes, networks, secrets, plugins, nodes and services. MITRE ATT&CK and compliance mappings (PCI-DSS, HIPAA, NIST, TSC) already ship with the product.
We did not write a single rule in local_rules.xml.
Verification: a real container lifecycle
We ran docker run → docker stop → docker rm against an image already on disk (no Docker Hub dependency) and watched alerts.json on the manager:
| Real action | Rule | Level | What you see |
|---|---|---|---|
| create | 87901 | 3 | Container … created |
| start | 87903 | 3 | Container … started |
kill (SIGTERM, from docker stop) |
87924 | 7 | received the action: kill |
| kill (SIGKILL, after the grace timeout) | 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 is 128+9: the container died from SIGKILL. Rule 87924 matches both kill and die, so a routine docker stop produces several level-7 alerts. That matters in the next section.
Real sample from 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" } }
}
}
}
Events landed tagged with agent.id: 008 and groups: ["docker"]. No custom rule in the path.
Why Docker alerts are not going to Telegram yet
custom-telegram.py filters on level >= 7 and on group (control_parental, adguard, hayabusa, aur_malware, urlscan). The docker group is not in that list. These alerts live in the dashboard and alerts.json; they do not hit the phone, including the level-7 kill / die ones.
That is on purpose. This host redeploys often with docker compose up -d --build. Every legitimate redeploy emits stop / kill / die / destroy and would fire level ≥ 7. Wiring the whole docker group into Telegram turns an n8n rebuild into a notification storm.
Watch the real volume in Grafana for a few days first. You will almost certainly want to match specific actions (a pull of an image you do not recognise, exec_start: bash on a production container) rather than the entire group. Same low-noise bar as the proactive homelab SOC and urlscan enrichment: signal, not theatre.
What it covers (and what it does not)
- The wodle only sees the Docker daemon on the host where the agent runs. It does not cover k3s-managed containers. Control-plane activity is the Kubernetes audit webhook.
- Without Telegram, visibility is pull (dashboard), not push. You have to go look.
- It does not read application logs inside the container. That is still a bind-mount plus a
<localfile>, not this wodle.
To disable: remove the wodle block (or set <disabled>yes</disabled>) and systemctl restart wazuh-agent.
Closing
You do not always need to rewrite rules to adopt a “new” Wazuh integration. Read the official ruleset for your version first. And when the integration needs a privileged socket, who holds that access matters as much as “does it work?”.
What you get: lifecycle visibility for host Docker (outside k3s), zero custom XML to maintain, and no Docker socket exposure added to the Wazuh manager.