Skip to content

Wazuh

Last reviewed: 2026-06-16

Purpose: Wazuh openโ€‘source security monitoring platform โ€” SIEM and XDR capabilities for threat detection, integrity monitoring, compliance, and active response.


Table of Contents


Architecture Overview

A production Wazuh deployment consists of four components:

Component Role
Wazuh Manager Central server that receives agent data, triggers alerts, and runs active response.
Wazuh Indexer Clustered OpenSearch node that stores and indexes security events.
Wazuh Dashboard OpenSearch Dashboards UI for visualisation, rule management, and reporting.
Wazuh Agent Lightweight endpoint daemon that collects logs, monitors file integrity, and runs commands.

All four components can run on a single host via Docker Compose for lab/evaluation setups, or be scaled out across multiple nodes for production.


Deploying Wazuh with Docker Compose

Prerequisites

  • Docker Engine โ‰ฅ 24.x and Docker Compose โ‰ฅ v2.20
  • Linux host (Ubuntu 22.04 / Debian 12 recommended)
  • At least 8 GB RAM, 4 vCPUs, 50 GB disk
  • Ports 443, 1514โ€“1516, 55000 (API) open on the firewall

Step 1 โ€” Set up directories and system limits

sudo sysctl -w vm.max_map_count=262144
echo 'vm.max_map_count=262144' | sudo tee -a /etc/sysctl.conf

Create the project layout:

mkdir -p wazuh-deployment/{config,wazuh-indexer-certs}
cd wazuh-deployment

Step 2 โ€” Clone the official configuration (or use the full example below)

The full docker-compose.yml in the next section is self-contained. Save it as docker-compose.yml inside wazuh-deployment/.

Step 3 โ€” Start the stack

docker compose up -d

Initial bootstrap takes 2โ€“4 minutes. Monitor logs:

docker compose logs -f

Step 4 โ€” Access the dashboard

Navigate to https://<host-ip>.

  • Username: admin
  • Password: SecretPassword (override via the OPENSEARCH_INITIAL_ADMIN_PASSWORD environment variable)

Complete docker-compose.yml Reference

Place this file at wazuh-deployment/docker-compose.yml:

version: '3.9'

services:
  # ---------------------------------------------------------------
  # Wazuh Indexer โ€” OpenSearch cluster (single node for demo)
  # ---------------------------------------------------------------
  wazuh-indexer:
    image: wazuh/wazuh-indexer:4.11.0
    hostname: wazuh-indexer
    container_name: wazuh-indexer
    restart: always
    ports:
      - "9200:9200"   # REST API
      - "9300:9300"   # Inter-node transport
    environment:
      - OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=SecretPassword
      - DISABLE_SECURITY_PLUGIN=false
    ulimits:
      nofile:
        soft: 65535
        hard: 65535
      memlock:
        soft: -1
        hard: -1
    volumes:
      - indexer-data:/var/lib/opensearch/data
      - ./config/wazuh-indexer.yml:/usr/share/opensearch/config/opensearch.yml
      - ./wazuh-indexer-certs:/usr/share/opensearch/config/certs
    networks:
      - wazuh-net

  # ---------------------------------------------------------------
  # Wazuh Manager โ€” central server (API + analysis + active response)
  # ---------------------------------------------------------------
  wazuh-manager:
    image: wazuh/wazuh-manager:4.11.0
    hostname: wazuh-manager
    container_name: wazuh-manager
    restart: always
    ports:
      - "1514:1514/udp"   # Agent communication (UDP)
      - "1515:1515/tcp"   # Agent enrollment (TCP)
      - "1516:1516/tcp"   # Agentless / syscheck
      - "55000:55000/tcp" # Wazuh RESTful API
    environment:
      - WAZUH_MANAGER_IP=0.0.0.0
    volumes:
      - manager-data:/var/ossec/data
    depends_on:
      wazuh-indexer:
        condition: service_healthy
    networks:
      - wazuh-net

  # ---------------------------------------------------------------
  # Wazuh Dashboard โ€” OpenSearch Dashboards UI
  # ---------------------------------------------------------------
  wazuh-dashboard:
    image: wazuh/wazuh-dashboard:4.11.0
    hostname: wazuh-dashboard
    container_name: wazuh-dashboard
    restart: always
    ports:
      - "443:5601"        # HTTPS UI
    environment:
      - OPENSEARCH_HOSTS=https://wazuh-indexer:9200
      - WAZUH_MANAGER_HOST=wazuh-manager
      - OPENSEARCH_SSL_VERIFY=false
    volumes:
      - ./config/wazuh-dashboard.yml:/usr/share/opensearch-dashboards/config/opensearch_dashboards.yml
    depends_on:
      wazuh-indexer:
        condition: service_healthy
    networks:
      - wazuh-net

volumes:
  indexer-data:
  manager-data:

networks:
  wazuh-net:
    driver: bridge

Required Config Files

config/wazuh-indexer.yml

path.data: /var/lib/opensearch/data
path.logs: /var/log/opensearch
network.host: 0.0.0.0
discovery.type: single-node
plugins.security.ssl.transport.pemcert_filepath: /usr/share/opensearch/config/certs/indexer.pem
plugins.security.ssl.transport.pemkey_filepath: /usr/share/opensearch/config/certs/indexer-key.pem
plugins.security.ssl.transport.pemtrustedcas_filepath: /usr/share/opensearch/config/certs/ca.pem
plugins.security.ssl.http.enabled: true
plugins.security.ssl.http.pemcert_filepath: /usr/share/opensearch/config/certs/indexer.pem
plugins.security.ssl.http.pemkey_filepath: /usr/share/opensearch/config/certs/indexer-key.pem
plugins.security.ssl.http.pemtrustedcas_filepath: /usr/share/opensearch/config/certs/ca.pem
plugins.security.allow_default_init_securityindex: true

config/wazuh-dashboard.yml

server.host: 0.0.0.0
server.port: 5601
opensearch.hosts: ["https://wazuh-indexer:9200"]
opensearch.ssl.verificationMode: none

Agent Deployment on Linux

Installation

On each endpoint you want to monitor:

# Import the Wazuh GPG key and add the repository
curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | sudo gpg --no-default-keyring --keyring gnupg-ring:/usr/share/keyrings/wazuh.gpg --import
sudo chmod 644 /usr/share/keyrings/wazuh.gpg

echo "deb [signed-by=/usr/share/keyrings/wazuh.gpg] https://packages.wazuh.com/4.x/apt/ stable main" | sudo tee /etc/apt/sources.list.d/wazuh.list

sudo apt update && sudo apt install -y wazuh-agent

Enrollment (register with manager)

Replace WAZUH_MANAGER_IP with your manager's IP address.

# Method 1 โ€” Auto-enrollment via /var/ossec/etc/ossec.conf
# Configure <client><server><address>WAZUH_MANAGER_IP</address></server></client>

# Method 2 โ€” Manual enrollment using agent-auth
sudo /var/ossec/bin/agent-auth -m WAZUH_MANAGER_IP -A <agent-name>
sudo systemctl restart wazuh-agent

Verify connectivity

On the manager container (or host):

sudo /var/ossec/bin/agent_control -l

You should see your agent listed with a status of Active.


Log Collection Configuration

File-based log monitoring

Edit the agent's ossec.conf (typically /var/ossec/etc/ossec.conf) to add <localfile> entries:

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

  <localfile>
    <log_format>syslog</log_format>
    <location>/var/log/syslog</location>
  </localfile>

  <localfile>
    <log_format>apache</log_format>
    <location>/var/log/apache2/access.log</location>
  </localfile>

  <!-- Multi-line JSON logs (e.g., Docker / containers) -->
  <localfile>
    <log_format>json</log_format>
    <location>/var/log/containers/*.log</location>
  </localfile>
</ossec_config>

Restart the agent after changes:

sudo systemctl restart wazuh-agent

Remote syslog collection (agentless)

On the manager, configure ossec.conf to listen for syslog:

<ossec_config>
  <remote>
    <connection>syslog</connection>
    <port>514</port>
    <protocol>udp</protocol>
    <allowed-ips>10.0.0.0/8</allowed-ips>
  </remote>
</ossec_config>

Active Response Setup

Active response lets the Wazuh manager execute a command on the agent when a rule triggers.

1. Define the active response command

In the manager's /var/ossec/etc/ossec.conf:

<ossec_config>
  <command>
    <name>block-ip</name>
    <executable>firewall-drop</executable>
    <timeout_allowed>yes</timeout_allowed>
  </command>

  <active-response>
    <command>block-ip</command>
    <location>local</location>
    <rules_id>3150,3151</rules_id>
    <timeout>300</timeout>
  </active-response>
</ossec_config>
  • firewall-drop is a built-in script that adds an iptables drop rule.
  • rules_id references rule IDs that should trigger the action.
  • timeout seconds after which the block is automatically removed.

2. Verify active response logs

tail -f /var/ossec/logs/active-response.log

3. Custom active response script

Place a script in /var/ossec/active-response/bin/ on the agent:

#!/bin/bash
# /var/ossec/active-response/bin/block-ip-custom.sh
# Arguments passed by Wazuh: $1 = action (add/delete), $3 = IP

ACTION=$1
IP=$3

case $ACTION in
  add)
    iptables -A INPUT -s "$IP" -j DROP
    ;;
  delete)
    iptables -D INPUT -s "$IP" -j DROP
    ;;
esac

Make it executable:

sudo chmod 750 /var/ossec/active-response/bin/block-ip-custom.sh
sudo chown root:wazuh /var/ossec/active-response/bin/block-ip-custom.sh

Common Rules and Decoders

Wazuh ships with thousands of pre-built rules and decoders. Understanding the rule ID ranges helps with tuning:

ID Range Category
001โ€“099 Agent connectivity / status
100โ€“199 Authentication failures (SSH, sudo, PAM)
200โ€“399 System integrity (file changes, process monitoring)
500โ€“599 Vulnerability events
600โ€“699 Security policy / compliance
700โ€“799 Multi-pattern rule groups
800โ€“899 Web server (Apache, Nginx, IIS)
900โ€“999 Firewall (iptables, nftables, pf)
1000โ€“1999 Application logs (MySQL, Postfix, Docker)
2000โ€“2999 Custom / user-defined rules
3000โ€“3999 Windows events (EventChannel, Sysmon)
5000โ€“5999 Active response triggers
6000โ€“6999 Vulnerability detection (CVE)
80000โ€“81000 GDPR / PCI DSS / HIPAA compliance rules

Custom Rule Example

Place custom rules in /var/ossec/etc/rules/local_rules.xml on the manager:

<group name="local,ssh,authentication,">
  <rule id="100100" level="10">
    <if_sid>5700</if_sid>
    <field name="dstuser">^root$</field>
    <description>SSH: Direct root login attempt (blocked by config but still logged)</description>
    <group>authentication_failed,pci_dss_10.2.4,</group>
  </rule>
</group>

Custom Decoder Example

Decoders parse raw logs into structured fields. Custom decoders go in /var/ossec/etc/decoders/local_decoder.xml:

<decoder name="custom-app">
  <parent>json</parent>
  <type>json</type>
</decoder>

Integration with Grafana

Wazuh events stored in the Wazuh Indexer (OpenSearch) can be queried from Grafana via the OpenSearch data source.

Step 1 โ€” Enable the Grafana OpenSearch plugin

grafana-cli plugins install grafana-opensearch-datasource
sudo systemctl restart grafana-server

Step 2 โ€” Add the data source in Grafana UI

  1. Configuration โ†’ Data Sources โ†’ Add data source โ†’ OpenSearch
  2. URL: https://<wazuh-indexer-ip>:9200
  3. Access: Server (default)
  4. Auth: Basic โ€” username admin, password SecretPassword
  5. Index name: wazuh-alerts-* (or wazuh-archives-* for raw logs)
  6. Time field name: @timestamp
  7. Enable TLS skip verify: ON

Step 3 โ€” Sample dashboard query (Lucene syntax)

rule.level:>=10 AND data.dstuser:root

Returns high-severity alerts where the destination user is root.

Step 4 โ€” Using the Wazuh Grafana App (optional)

grafana-cli plugins install wazuh-wazuh-app

After installation, enable the Wazuh app in Configuration โ†’ Plugins โ†’ Wazuh โ†’ Enable. This gives pre-built Wazuh dashboards.

Step 5 โ€” Query with cURL (API testing)

curl -k -u admin:SecretPassword \
  "https://<wazuh-indexer-ip>:9200/wazuh-alerts-*/_search?pretty" \
  -H 'Content-Type: application/json' \
  -d '{
    "query": {
      "bool": {
        "filter": [
          { "range": { "@timestamp": { "gte": "now-1h" } } },
          { "term": { "rule.level": 12 } }
        ]
      }
    }
  }'

Troubleshooting

Agent not connecting

# On the agent
sudo /var/ossec/bin/agent_control -l
sudo tail -100 /var/ossec/logs/ossec.log

# On the manager
docker compose logs wazuh-manager | grep -i "auth\|connection"

Indexer not starting

Check the heap and memory map limit:

docker compose logs wazuh-indexer | tail -30
sudo sysctl vm.max_map_count  # must be >= 262144
docker compose restart wazuh-indexer

Dashboard returns "Security plugin not configured"

Re-initialise the security index:

docker exec -it wazuh-indexer bash
chmod +x /usr/share/opensearch/plugins/opensearch-security/tools/securityadmin.sh
/usr/share/opensearch/plugins/opensearch-security/tools/securityadmin.sh \
  -cd /usr/share/opensearch/plugins/opensearch-security/securityconfig \
  -icl -nhnv \
  -cacert /usr/share/opensearch/config/certs/ca.pem \
  -cert /usr/share/opensearch/config/certs/admin.pem \
  -key /usr/share/opensearch/config/certs/admin-key.pem

Resetting the admin password

docker exec -it wazuh-indexer bash
export OPENSEARCH_INITIAL_ADMIN_PASSWORD=<new-password>
/usr/share/opensearch/plugins/opensearch-security/tools/hash.sh -p <new-password>
# Use the returned hash in the internal_users.yml, then run securityadmin.sh

References