Wazuh monitors containers in two places. On a Docker host, the docker-listener wodle runs inside the agent, subscribes to the Docker event stream and turns container and image activity into findings. In Kubernetes, the official deployment documentation supplies a DaemonSet manifest that runs one agent per node using the wazuh/wazuh-agent image, plus a sidecar pattern for monitoring a single application. Both are agent-side configurations, and neither needs an agent inside every container.
What you need
- A Wazuh agent on the Docker host or a Kubernetes cluster with
kubectland kustomize support, which is included inkubectl1.14 and above. - Python 3 and
pipon the Docker host, for the listener’s Python dependencies. - The manager’s enrolment password from
/var/wazuh-manager/etc/authd.pass, for the Kubernetes route. - Groups created on the manager before any agent enrols. See agent groups.
If your containers run in a managed cloud, collect the control plane’s own audit trail too. Cloud log collection covers CloudTrail, Azure and Google Cloud, which is where EKS, AKS and GKE control plane activity lands.
Monitor a Docker host
Install the dependencies
The listener is a Python wodle, and it needs the Docker SDK on the host. Install Python and pip first:
sudo apt install -y python3 python3-pip
Then the pinned library versions from the documentation:
pip3 install docker==7.1.0 urllib3==1.26.20 requests==2.32.2 --break-system-packages
The pins matter. The documentation adds an explicit instruction to delete any newer version of urllib3 if one is present, which tells you the compatibility window is narrow. If you prefer a virtual environment to modifying the system Python, update the shebang of /var/ossec/wodles/docker/DockerListener to point at that interpreter.
Enable the module
Add the wodle to /var/ossec/etc/ossec.conf on the Docker host:
<wodle name="docker-listener">
<interval>2m</interval>
<attempts>5</attempts>
<run_on_start>yes</run_on_start>
<disabled>no</disabled>
</wodle>
Restart the agent:
sudo systemctl restart wazuh-agent
The four options are worth reading in their documented sense rather than the obvious one, and the Docker monitoring page carries the full table. disabled defaults to no, so the block’s presence is what matters. attempts is the number of attempts to execute the listener if it fails, defaulting to 5. interval is described as the waiting time to rerun the listener in case it fails, defaulting to 1m, which is not a polling interval: the listener subscribes to a stream, so this is a restart backoff. run_on_start defaults to yes.
Scheduling options day, wday and time also exist and follow the same rules as other wodles, with the caveat that setting day forces the interval to a multiple of months and setting wday forces it to a multiple of weeks. For a stream subscriber, leaving them unset is almost always right.
See what it produces
Generate some activity on the host:
docker run -d --name test-container httpd
docker stop test-container
docker rm test-container
docker rmi httpd
View the results in the Threat Hunting module, or on the Findings tab of the Docker dashboard. The documentation publishes the full finding for an image pull, and it is worth reading because it shows exactly which fields you can filter and pivot on:
{
"container": {
"image": {
"name": "httpd",
"tag": ["latest"]
}
},
"orchestrator": {
"resource": {
"type": "image"
}
},
"wazuh": {
"integration": {
"name": "docker",
"category": "system-activity"
},
"rule": {
"level": "medium",
"title": "Docker image pulled from external or unknown registry - httpd",
"tags": ["medium", "docker", "attack.initial-access", "attack.t1195.002"]
}
},
"event": {
"code": "pull",
"action": "package-installed",
"dataset": "docker.events",
"outcome": "success"
}
}
That is an abridged view of the document the documentation prints in full. Four things to take from it.
The integration is docker and its category is system-activity, so Docker findings live in wazuh-findings-v5-system-activity, not in a container-specific index. The rule carries a sigma_id and a structured mitre block, in this case T1195.002 for compromising a software supply chain. The compliance block is populated out of the box across PCI DSS, ISO 27001, HIPAA, NIST 800-53 and 800-171, CMMC, GDPR, FedRAMP, NIS2 and TSC, which is a real head start if you are evidencing controls. And event.dataset is docker.events, which is the cheapest filter to reach for in Discover.
Run the agent as a Kubernetes DaemonSet
The documentation describes two agent deployment models inside Kubernetes. A DaemonSet runs one agent on each node, monitoring the node and all containers on that node, and is described as the most common approach for full-cluster monitoring: each node runs one agent, so new nodes are covered without manual intervention. A sidecar runs the agent as a companion container beside a specific application pod, for targeted monitoring of a sensitive workload.
Start with the DaemonSet. Reach for a sidecar only when one application needs isolated log collection that the node agent cannot give it.
What the manifest actually does
The published manifest is longer than you might expect, and the length is load-bearing. It runs five init containers before the agent starts:
- Clean stale PID and lock files from the previous pod.
- Seed
/var/ossecfrom the image into the hostPath, but only on the first run, guarded by a check for/agent/bin. - Fix ownership and permissions, setting
999:999onetc,logs,queue,var,rids,tmpandactive-response, and0:0onbinandlib. - Write
ossec.confwith the manager endpoint, the registration server and the node name as the agent name. - Copy
authd.passin from a Kubernetes Secret and set it to0:999mode0640.
The agent state lives on a hostPath at /var/lib/wazuh, mounted at /var/ossec in the container. That is the design decision that makes the whole thing work: without it, every pod restart would be a fresh enrolment and your agent list would fill with duplicates.
The manifest also mounts the host’s /var/log read-only, and the non-EKS variant additionally mounts /var/run/docker.sock as a read-only Socket volume. That socket mount is the only difference between the two published variants, and it is the difference between a node agent that can see Docker events and one that cannot.
The manifest is in the companion files for this lesson, reproduced from the Kubernetes deployment page rather than rewritten.
Deploy it
Create the namespace:
kubectl create namespace wazuh-daemonset
Create the Secret holding the enrolment password:
kubectl create secret generic wazuh-authd-pass -n wazuh-daemonset --from-literal=authd.pass=password
The documentation notes that the default enrolment password is password, stored in /var/wazuh-manager/etc/authd.pass on the manager. Read your actual value from that file rather than assuming the default.
Apply the manifest, after replacing the external IP placeholders for the wazuh-events and wazuh-registration services:
kubectl apply -f wazuh-agent-daemonset.yaml
Check that one pod is running per node:
kubectl get pods -n wazuh-daemonset -o wide
Groups and enrolment order
Two rules from elsewhere in 5.0 bite hard here, and neither is mentioned in the Kubernetes pages.
Groups must exist on the manager before an agent enrols into one, and must be declared in the agent’s <enrollment><groups> block. Enrolment is rejected otherwise. Create the group first, then deploy the DaemonSet.
Agent names must be unique. The manifest uses spec.nodeName as the agent name, which is the right choice: it is stable across pod restarts, unlike a pod name. If you change it to something ephemeral you will generate a new agent record on every restart. Agent enrolment covers duplicate handling.
Kubernetes audit logs
The 5.0 beta documentation has no Kubernetes audit log page. The container security section describes Docker monitoring and states that Wazuh can also monitor Kubernetes infrastructure, but the page that covered the audit log configuration is absent from the beta tree.
What you can do with documented pieces: the API server writes its audit log to a file on the control plane node, and a Wazuh agent reads files with <localfile>. That reduces the problem to ordinary log collection, which is covered in Wazuh log collection. The part that is genuinely not documented for 5.0 is the decoder and ruleset: there is no Kubernetes integration listed in the beta content, so normalising the audit JSON into the Wazuh Common Schema is work you would write yourself as a custom integration. Writing a custom decoder is the starting point.
I would not build that on a beta. Note the gap, keep the audit log flowing to a file, and re-check the content catalogue at GA.
Container FIM, and why it disappoints
File integrity monitoring on containers is usually the wrong instinct, and it is worth saying why before someone spends a week on it.
A container image is immutable. The interesting layer is the writable one on top, and it is ephemeral by design: when the container goes, so does the evidence. A node agent sees only what is mounted into it, which in the documented DaemonSet is the host’s /var/log read-only and the agent’s own hostPath. It does not see other containers’ layered filesystems, and making it see them means mounting the container runtime’s storage directory into the agent, which is a large privilege grant for a small return.
Put FIM on the host paths that matter: the container runtime configuration, the kubelet configuration, the manifests directory, the certificate store. Those are where an attacker makes changes that persist. For the containers themselves, the Docker listener’s lifecycle events and the image pull detections give you more signal per unit of effort than any FIM configuration will. FIM setup covers scoping.
Common errors
The Docker listener produces nothing
Three checks, in order. Is the Docker service running and is the agent user able to reach the Docker socket? Are the Python dependencies at the pinned versions, including the urllib3 downgrade? And is the docker integration enabled in the ruleset, in the same way cloud integrations must be enabled before they produce findings? Confirm events are arriving by querying wazuh-events-v5-system-activity in Discover before you assume the module is broken.
The listener starts and then stops
Check attempts. The module retries a failed execution the configured number of times, five by default, and then stops. A Docker daemon that was not yet up when the agent started will exhaust the attempts and stay down. Restart the agent after the Docker service, or set run_on_start to no and let the schedule bring it up.
ImagePullBackOff on the DaemonSet pods
The manifest pins wazuh/wazuh-agent:5.0.0-beta5-latest. Beta tags move and are not guaranteed to persist. Confirm the tag exists before you blame the cluster, and pin to a digest if the deployment matters.
Agents duplicate on every pod restart
The hostPath volume is missing or is pointing somewhere that does not survive. The manifest keeps agent state at /var/lib/wazuh on the node with type: DirectoryOrCreate, and the seed init container only populates it when /agent/bin is absent. If state is not persisting, the agent re-enrols with a new key each time.
Enrolment rejected for a new node
The group named in the agent configuration does not exist on the manager. Groups must be created before enrolment in 5.0, and the manifest’s enrolment block will fail rather than fall back to default.
The agent pod runs but never reaches Active
Work outwards from the pod. Check the agent’s own log inside the container, then the network policies. The Kubernetes deployment ships a Default-deny-all policy that blocks everything not explicitly allowed, with separate policies for ingress to the manager master on the enrolment port and to the manager workers on the events port. A DaemonSet agent in its own namespace is not covered by those policies.
Next: The Wazuh API with Python: Agents, Groups and Findings.
Files for this lesson
Every file this lesson refers to, exactly as used. Open a file to read it in the browser, or download the set as a zip. Download wazuh-docker-kubernetes-monitoring.zip. The full set for all lessons is at wazuh-tutorial-files.zip.
Frequently asked questions
How does Wazuh monitor Docker containers?
With the Docker listener module, a wodle that runs on the Wazuh agent installed on the Docker host. It subscribes to the Docker event stream and forwards container and image events to the manager. It needs Python 3 and the docker library on the host, and it is disabled until you add the wodle block to the agent configuration.
Do I need an agent inside every container?
No. The documented models are an agent on the Docker host, a DaemonSet with one agent per Kubernetes node, or a sidecar agent beside a specific application pod. An agent per container is not one of them, and it scales badly for the same reason a sidecar does.
What events does the Wazuh Docker listener produce?
Container and image lifecycle activity: pulls, starts, stops, removals and similar Docker daemon events. In 5.0 these are decoded by the docker integration into the system-activity category, with container and orchestrator fields populated in the Wazuh Common Schema.
Can Wazuh run as a Kubernetes DaemonSet?
Yes. The Kubernetes deployment documentation gives a full DaemonSet manifest built on the wazuh/wazuh-agent image, with init containers that seed the agent tree onto a hostPath, write ossec.conf, and copy the enrolment password in from a Kubernetes Secret. One agent runs on each node.
Does FIM work inside a container?
It sees only what is mounted into the agent. The documented DaemonSet mounts the host's /var/log read-only and keeps agent state on a hostPath, so a node agent watches host paths rather than the layered filesystems of other containers. Container images are also immutable by design, which makes conventional FIM on them close to meaningless.