Life and Shell

Writing · security

Running the Wazuh agent in Docker

By Mattias Hemmingsson · · 4 min read

The normal way to run the Wazuh agent is to install the package on every server. That works, but it ties the agent to the host: its libraries, its package manager, its upgrade cycle. A host upgrade can break the agent, and an agent upgrade touches the host.

We run the agent in a Docker container instead. It sees the host through read-only mounts and sends everything to our Wazuh manager on Kubernetes (the collector from the previous post).

Why a container

  • No dependency on the host system. The agent brings its own Debian userland and Python. It doesn't matter if the host is Ubuntu, Debian, Fedora or a minimal cloud image, as long as it runs Docker.
  • Pinned version. Wazuh refuses agents that are newer than the manager. We learned this when a fresh build pulled 4.14.10 against a 4.14.0 manager and never connected. The image now pins the version with a build arg, so the agent and manager always match.
  • Clean upgrades and removal. Upgrade = new image tag. Removal = docker compose down. Nothing is left on the host.
  • Several agents on one host. Run one agent for file integrity, another for a noisy application log, each with its own group and config. It is just another service in the compose file.

The image

The node image is ghcr.io/samma-io/wazuh-agent-node, built from Dockerfile.agent in samma-io/wazuh-agent:

  • Debian trixie-slim with wazuh-agent=4.14.0-1
  • osquery 5.24.1 (off by default)
  • An ossec.conf template filled in from environment variables at start

All paths in the config point at /hostfs/..., so the agent reads the host, not the container:

<syscheck>
  <directories>/hostfs/etc,/hostfs/usr/bin,/hostfs/usr/sbin</directories>
  <directories>/hostfs/bin,/hostfs/sbin,/hostfs/boot</directories>
</syscheck>

<localfile>
  <log_format>syslog</log_format>
  <location>/hostfs/var/log/syslog</location>
</localfile>
<localfile>
  <log_format>syslog</log_format>
  <location>/hostfs/var/log/auth.log</location>
</localfile>

auth.log is there on purpose. Ubuntu keeps login and sudo messages out of syslog, so without it you never see failed logins or account changes.

docker compose

services:
  wazuh-agent:
    image: ghcr.io/samma-io/wazuh-agent-node:main
    restart: unless-stopped
    volumes:
      - /etc:/hostfs/etc:ro
      - /bin:/hostfs/bin:ro
      - /usr/bin:/hostfs/usr/bin:ro
      - /usr/sbin:/hostfs/usr/sbin:ro
      - /boot:/hostfs/boot:ro
      - /var/log:/hostfs/var/log:ro
    environment:
      - MANAGER_URL=10.0.0.33      # any cluster node
      - MANAGER_PORT=30515         # NodePort -> manager 1515 (enrollment)
      - SERVER_URL=10.0.0.33       # any cluster node
      - SERVER_PORT=30514          # NodePort -> worker 1514 (events)
      - NAME=web-01                # a random suffix is added
      - GROUP=default              # must exist in Wazuh
      - ENROL_TOKEN=change-me      # the manager's enrollment password
docker compose up -d
docker compose logs -f wazuh-agent

The mounts are read-only. The agent can read logs and hash files, but it cannot change anything on the host.

Connect it to Wazuh on Kubernetes

Agent traffic is plain TCP, so it can't go through an HTTP ingress. Our Wazuh chart has services.agentNodePorts.enabled: true, which opens exactly two NodePorts and nothing else:

Purpose Service Port in pod NodePort
Enrollment wazuh-agent-enroll → master 1515 30515
Events wazuh-agent-events → worker 1514 30514

Check them in the cluster:

kubectl get svc -n samma | grep wazuh-agent

Point MANAGER_URL and SERVER_URL at any node's IP. ENROL_TOKEN is the manager's authd.pass, set in the chart's credentials. The group in GROUP has to exist in Wazuh before the agent enrolls, or the manager rejects it.

Always set all four host/port variables. The entrypoint's defaults are localhost, which will never be what you want in a container.

When the agent starts you should see it enroll on 1515, get a key, and connect on 1514. On the manager side that produces Wazuh rule 501, "new agent connected", which in Samma also fires a Sigma alert. An unexpected new agent is worth knowing about, since enrollment only needs a shared password.

Many agents on one host

The entrypoint appends a random 10-character suffix to NAME. Every container that starts gets a unique agent name, so you can run several from the same compose file:

services:
  agent-fim:
    image: ghcr.io/samma-io/wazuh-agent-node:main
    environment: [NAME=web-01-fim, GROUP=fim, ...]
    volumes: [/etc:/hostfs/etc:ro, /usr/bin:/hostfs/usr/bin:ro]

  agent-nginx:
    image: ghcr.io/samma-io/wazuh-agent-node:main
    environment: [NAME=web-01-nginx, GROUP=nginx, ...]
    volumes: [/var/log/nginx:/hostfs/var/log/nginx:ro]

Each one gets its own group, so the manager can push a different shared config to each. On a big host this splits the work, and a noisy log no longer delays file integrity checks.

The flip side of the random suffix: a recreated container enrolls as a new agent. Clean up disconnected agents in the manager now and then.

Custom config

The config is a template, config/ossec_node.tpl, run through envsubst at every start. To watch other paths or logs, copy it, edit it, and mount your version over /opt/ossec/ossec.tpl:

    volumes:
      - ./my-ossec.tpl:/opt/ossec/ossec.tpl:ro

osquery inside the agent

The image includes osquery. Set OSQUERY_DISABLED=no and the agent's osquery module runs a few scheduled queries (heartbeat, listening ports, processes) and sends the results as Wazuh rule 24010. osquery sees what the container sees, so add --pid=host and --network=host if you want the host's processes and ports.

For a full osquery setup with real-time process, socket and file events, we built a separate client with its own lane into Samma. That is the next post.

Filed underwazuh · docker · siem · security · samma