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 kubectl and kustomize support, which is included in kubectl 1.14 and above.
  • Python 3 and pip on 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:

  1. Clean stale PID and lock files from the previous pod.
  2. Seed /var/ossec from the image into the hostPath, but only on the first run, guarded by a check for /agent/bin.
  3. Fix ownership and permissions, setting 999:999 on etc, logs, queue, var, rids, tmp and active-response, and 0:0 on bin and lib.
  4. Write ossec.conf with the manager endpoint, the registration server and the node name as the agent name.
  5. Copy authd.pass in from a Kubernetes Secret and set it to 0:999 mode 0640.

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.