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
- Deploying Wazuh with Docker Compose
- Complete docker-compose.yml Reference
- Agent Deployment on Linux
- Log Collection Configuration
- Active Response Setup
- Common Rules and Decoders
- Integration with Grafana
- Troubleshooting
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:
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
Initial bootstrap takes 2โ4 minutes. Monitor logs:
Step 4 โ Access the dashboard
Navigate to https://<host-ip>.
- Username:
admin - Password:
SecretPassword(override via theOPENSEARCH_INITIAL_ADMIN_PASSWORDenvironment 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):
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:
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-dropis a built-in script that adds an iptables drop rule.rules_idreferences rule IDs that should trigger the action.timeoutseconds after which the block is automatically removed.
2. Verify active response logs
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:
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
Step 2 โ Add the data source in Grafana UI
- Configuration โ Data Sources โ Add data source โ OpenSearch
- URL:
https://<wazuh-indexer-ip>:9200 - Access: Server (default)
- Auth: Basic โ username
admin, passwordSecretPassword - Index name:
wazuh-alerts-*(orwazuh-archives-*for raw logs) - Time field name:
@timestamp - Enable TLS skip verify: ON
Step 3 โ Sample dashboard query (Lucene syntax)
Returns high-severity alerts where the destination user is root.
Step 4 โ Using the Wazuh Grafana App (optional)
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