Wazuh + Suricata — SIEM and Network IDS for a Homelab
1. Why Both?
Section titled “1. Why Both?”Suricata and Wazuh are complementary, not competing:
| Wazuh | Suricata | |
|---|---|---|
| Sees | Host events — logins, file changes, config drift, running processes, agent reports | Network packets on a monitored interface |
| Detects | Policy violations, FIM hits, rootkits, agent-based attacks | Known malicious patterns in traffic (Emerging Threats ruleset) |
| Outputs | Structured alerts in OpenSearch, dashboard, API | eve.json — a firehose of events and alerts |
| Blind spots | Anything that happens on the wire | Anything encrypted, or off the monitored interface |
Run both and Wazuh becomes the single pane of glass for everything: its manager ingests Suricata’s eve.json via a local file decoder and presents the alerts alongside host events, correlated by time and source IP.
2. Architecture
Section titled “2. Architecture”┌─────────────┐ ┌────────────────┐ ┌──────────────┐│ Suricata │───────▶│ eve.json │───────▶│ Wazuh Manager││ (host NIC) │ │ (shared file) │ │ ossec.conf │└─────────────┘ └────────────────┘ │ localfile │ └──────┬───────┘┌─────────────┐ ││ Wazuh Agents│─────────────────────────────────────────┤ (filebeat)│ on clients │ TLS 1514 ▼└─────────────┘ ┌──────────────┐ │Wazuh Indexer │ │ (OpenSearch) │ └──────┬───────┘ │ ┌──────▼───────┐ │Wazuh Dashboard│ │(OSD + plugins)│ └──────────────┘Three Wazuh services:
- wazuh-indexer — OpenSearch fork. Stores every alert and event. The heavy one (1–2 GB JVM heap).
- wazuh-manager — The brain. Runs decoders and rules on incoming events, talks to agents on port 1514, ships output to the indexer via filebeat.
- wazuh-dashboard — OpenSearch Dashboards fork with Wazuh’s plugins baked in. Port 5601 (HTTPS).
Plus Suricata running on the host network so it can see real interface traffic.
3. Certificates (Do This First)
Section titled “3. Certificates (Do This First)”Every Wazuh-to-Wazuh connection is mTLS. There’s no way to skip this — the containers refuse to come up without certs. Generate them with Wazuh’s own tool before bringing the stack up:
# Pull the cert generator and the config templatecurl -sO https://packages.wazuh.com/4.14/wazuh-certs-tool.shcurl -sO https://packages.wazuh.com/4.14/config.yml
# Edit config.yml — set node names and IPs:# - wazuh-indexer → 192.168.1.248 (or the container name if single-host)# - wazuh-manager → same# - wazuh-dashboard → samebash wazuh-certs-tool.sh -A
# Put them where the stack expects themsudo mkdir -p /mnt/nfs/docker/docker/wazuh/config/wazuh_indexer_ssl_certssudo mv wazuh-certificates/* /mnt/nfs/docker/docker/wazuh/config/wazuh_indexer_ssl_certs/You end up with: root-ca.pem, root-ca-manager.pem, wazuh-indexer.pem + key, wazuh-manager.pem + key, wazuh-dashboard.pem + key.
4. The Compose File
Section titled “4. The Compose File”services: wazuh-indexer: image: wazuh/wazuh-indexer:4.14.3 container_name: wazuh-indexer restart: unless-stopped ulimits: memlock: { soft: -1, hard: -1 } nofile: { soft: 65536, hard: 65536 } environment: - "OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g" volumes: - wazuh_indexer_data:/var/lib/wazuh-indexer - /mnt/nfs/docker/docker/wazuh/config/wazuh_indexer_ssl_certs/wazuh-indexer.pem:/usr/share/wazuh-indexer/config/certs/wazuh-indexer.pem:ro - /mnt/nfs/docker/docker/wazuh/config/wazuh_indexer_ssl_certs/wazuh-indexer-key.pem:/usr/share/wazuh-indexer/config/certs/wazuh-indexer-key.pem:ro - /mnt/nfs/docker/docker/wazuh/config/wazuh_indexer_ssl_certs/root-ca.pem:/usr/share/wazuh-indexer/config/certs/root-ca.pem:ro - /mnt/nfs/docker/docker/wazuh/config/wazuh_indexer/wazuh.indexer.yml:/usr/share/wazuh-indexer/config/opensearch.yml:ro networks: [security]
wazuh-manager: image: wazuh/wazuh-manager:4.14.3 container_name: wazuh-manager restart: unless-stopped ulimits: memlock: { soft: -1, hard: -1 } nofile: { soft: 655360, hard: 655360 } environment: - INDEXER_URL=https://wazuh-indexer:9200 - INDEXER_USERNAME=admin - INDEXER_PASSWORD=admin - FILEBEAT_SSL_VERIFICATION_MODE=certificate - SSL_CERTIFICATE_AUTHORITIES=/etc/ssl/root-ca.pem - SSL_CERTIFICATE=/etc/ssl/filebeat.pem - SSL_KEY=/etc/ssl/filebeat-key.pem - API_USERNAME=wazuh-wui - API_PASSWORD=MyS3cr37P450r.*- ports: - "1514:1514" # agent registration + event ingestion - "1515:1515" # agent enrolment - "514:514/udp" # syslog - "55000:55000" # Wazuh API volumes: - wazuh_api_configuration:/var/ossec/api/configuration - wazuh_etc:/var/ossec/etc - wazuh_logs:/var/ossec/logs - wazuh_queue:/var/ossec/queue - wazuh_var_multigroups:/var/ossec/var/multigroups - wazuh_integrations:/var/ossec/integrations - wazuh_active_response:/var/ossec/active-response/bin - wazuh_agentless:/var/ossec/agentless - wazuh_wodles:/var/ossec/wodles - wazuh_filebeat_etc:/etc/filebeat - wazuh_filebeat_var:/var/lib/filebeat - /mnt/nfs/docker/docker/wazuh/config/wazuh_indexer_ssl_certs/root-ca-manager.pem:/etc/ssl/root-ca.pem:ro - /mnt/nfs/docker/docker/wazuh/config/wazuh_indexer_ssl_certs/wazuh-manager.pem:/etc/ssl/filebeat.pem:ro - /mnt/nfs/docker/docker/wazuh/config/wazuh_indexer_ssl_certs/wazuh-manager-key.pem:/etc/ssl/filebeat-key.pem:ro # Mount Suricata's log dir so the manager can tail eve.json - /mnt/nfs/docker/docker/suricata/logs:/var/log/suricata:ro networks: [security]
wazuh-dashboard: image: wazuh/wazuh-dashboard:4.14.3 container_name: wazuh-dashboard restart: unless-stopped ports: - "5601:443" environment: - INDEXER_USERNAME=admin - INDEXER_PASSWORD=admin - WAZUH_API_URL=https://wazuh-manager - DASHBOARD_USERNAME=kibanaserver - DASHBOARD_PASSWORD=kibanaserver - API_USERNAME=wazuh-wui - API_PASSWORD=MyS3cr37P450r.*- - RUN_AS=false volumes: - /mnt/nfs/docker/docker/wazuh/config/wazuh_indexer_ssl_certs/wazuh-dashboard.pem:/usr/share/wazuh-dashboard/certs/wazuh-dashboard.pem:ro - /mnt/nfs/docker/docker/wazuh/config/wazuh_indexer_ssl_certs/wazuh-dashboard-key.pem:/usr/share/wazuh-dashboard/certs/wazuh-dashboard-key.pem:ro - /mnt/nfs/docker/docker/wazuh/config/wazuh_indexer_ssl_certs/root-ca.pem:/usr/share/wazuh-dashboard/certs/root-ca.pem:ro - /mnt/nfs/docker/docker/wazuh/config/wazuh_dashboard/opensearch_dashboards.yml:/usr/share/wazuh-dashboard/config/opensearch_dashboards.yml:ro depends_on: [wazuh-indexer, wazuh-manager] networks: [security, proxy]
suricata: image: jasonish/suricata:latest container_name: suricata restart: unless-stopped network_mode: host cap_add: [NET_ADMIN, NET_RAW, SYS_NICE] entrypoint: ["/bin/sh", "-c", "suricata-update && suricata -i ens18"] volumes: - suricata_lib:/var/lib/suricata - /mnt/nfs/docker/docker/suricata/logs:/var/log/suricata
volumes: suricata_lib: wazuh_indexer_data: wazuh_api_configuration: wazuh_etc: wazuh_logs: wazuh_queue: wazuh_var_multigroups: wazuh_integrations: wazuh_active_response: wazuh_agentless: wazuh_wodles: wazuh_filebeat_etc: wazuh_filebeat_var:
networks: security: driver: bridge proxy: external: trueReplace ens18 with your actual interface (ip -br a to find it). Suricata must run on host networking to see real packets — inside a Docker network it’d only see the container’s tiny virtual world.
5. Indexer & Dashboard Configuration
Section titled “5. Indexer & Dashboard Configuration”The two config files pointed at by the compose mounts:
network.host: "0.0.0.0"node.name: "wazuh-indexer"cluster.initial_master_nodes: ["wazuh-indexer"]cluster.name: "wazuh-cluster"discovery.type: single-node
plugins.security.ssl.transport.pemcert_filepath: certs/wazuh-indexer.pemplugins.security.ssl.transport.pemkey_filepath: certs/wazuh-indexer-key.pemplugins.security.ssl.transport.pemtrustedcas_filepath: certs/root-ca.pemplugins.security.ssl.transport.resolve_hostname: falseplugins.security.ssl.transport.enforce_hostname_verification: false
plugins.security.ssl.http.enabled: trueplugins.security.ssl.http.pemcert_filepath: certs/wazuh-indexer.pemplugins.security.ssl.http.pemkey_filepath: certs/wazuh-indexer-key.pemplugins.security.ssl.http.pemtrustedcas_filepath: certs/root-ca.pem
plugins.security.authcz.admin_dn: - "CN=admin,OU=Wazuh,O=Wazuh,L=California,C=US"plugins.security.nodes_dn: - "CN=wazuh-indexer,OU=Wazuh,O=Wazuh,L=California,C=US"
plugins.security.allow_default_init_securityindex: trueplugins.security.restapi.roles_enabled: ["all_access", "security_rest_api_access"]server.host: 0.0.0.0server.port: 443opensearch.hosts: https://wazuh-indexer:9200opensearch.ssl.verificationMode: certificateopensearch.username: kibanaserveropensearch.password: kibanaserveropensearch.requestHeadersAllowlist: [authorization, securitytenant]
server.ssl.enabled: trueserver.ssl.certificate: /usr/share/wazuh-dashboard/certs/wazuh-dashboard.pemserver.ssl.key: /usr/share/wazuh-dashboard/certs/wazuh-dashboard-key.pem
opensearch_security.multitenancy.enabled: trueopensearch_security.readonly_mode.roles: [kibana_read_only]
uiSettings.overrides.defaultRoute: /app/wz-home6. Bring Up the Stack
Section titled “6. Bring Up the Stack”cd /opt/stacks/securitydocker compose up -d# Wait ~2 minutes for the indexer's security plugin to initialise on first bootdocker logs -f wazuh-indexer | grep "security index"Then hit https://<host>:5601. Browser warning (self-signed), accept, log in with admin / admin.
Change the admin password immediately:
docker exec -it wazuh-indexer bash -c " export JAVA_HOME=/usr/share/wazuh-indexer/jdk bash /usr/share/wazuh-indexer/plugins/opensearch-security/tools/wazuh-passwords-tool.sh \ -au admin -ap admin -a -A -n 'NewP4ssw0rd!'"7. Wiring Suricata Into Wazuh
Section titled “7. Wiring Suricata Into Wazuh”Suricata writes eve.json to /var/log/suricata/eve.json (NDJSON — one JSON object per event). Because the compose mounts that directory into the Wazuh manager, you just need to tell ossec.conf to tail it:
<ossec_config> <localfile> <log_format>json</log_format> <location>/var/log/suricata/eve.json</location> </localfile></ossec_config>Restart the manager: docker exec wazuh-manager /var/ossec/bin/wazuh-control restart.
Wazuh ships with decoders for Suricata’s eve.json format out of the box (0470-suricata_decoders.xml) — alerts appear in the dashboard under their own rule group within minutes.
8. Installing Wazuh Agents
Section titled “8. Installing Wazuh Agents”Host-side telemetry is what really earns Wazuh its keep. Install the agent on anything you care about — laptops, NAS, other VMs.
# On a Debian/Ubuntu client:curl -s https://packages.wazuh.com/key/GPG-KEY-WAZUH | sudo apt-key add -echo "deb https://packages.wazuh.com/4.x/apt/ stable main" | sudo tee /etc/apt/sources.list.d/wazuh.listsudo apt update && sudo apt install wazuh-agent
sudo sed -i 's|<address>MANAGER_IP</address>|<address>192.168.1.248</address>|' \ /var/ossec/etc/ossec.conf
sudo systemctl enable --now wazuh-agentAgents auto-enrol via port 1515 on first start. In the dashboard: Agents → Deploy new agent walks you through it with the right commands copy-pasteable.
9. Using the Dashboard
Section titled “9. Using the Dashboard”The sections you’ll actually open
Section titled “The sections you’ll actually open”| Section | What’s there |
|---|---|
| Threat Hunting | Free-text search over every alert. Start here. |
| Security Events | Rule-level timeline. See authentication failures, firewall drops, custom rules firing. |
| Integrity Monitoring (FIM) | File changes in watched directories (default: /etc, /bin, /usr/bin…) |
| Vulnerability Detection | CVE scan against installed packages on each agent |
| MITRE ATT&CK | Maps events to MITRE tactics/techniques — useful for reporting |
Useful queries
Section titled “Useful queries”In Threat Hunting:
rule.level:>=10 # anything criticalrule.groups:authentication_failed AND agent.name:nasrule.groups:suricata AND data.alert.severity:1 # Suricata high-sev onlydata.srcip:185.156.73.* # all events from an ip rangeTuning — kill the noise
Section titled “Tuning — kill the noise”Straight out of the box Wazuh will show you things like “ssh login succeeded for a valid user” as events. That’s correct but useless. Go to rules and raise thresholds, or disable noisy rules entirely.
Common targets on a homelab:
5716— “SSH authentication success” — level 3, usually fine to leave31100/31101— web-traffic noise from CrowdSec’s bans533— “listened ports status changed” — tripped by Docker’s churn
10. File Integrity Monitoring (FIM)
Section titled “10. File Integrity Monitoring (FIM)”FIM is the single most useful Wazuh feature for a homelab. Watch a directory; any change (add / modify / delete) fires an alert with a diff.
In ossec.conf:
<syscheck> <frequency>43200</frequency>
<directories check_all="yes" realtime="yes">/etc,/usr/bin,/usr/sbin</directories> <directories check_all="yes" realtime="yes">/opt/stacks</directories> <directories check_all="yes" report_changes="yes">/var/ossec/etc</directories>
<ignore>/etc/mtab</ignore> <ignore>/etc/random-seed</ignore></syscheck>realtime="yes" uses inotify for sub-second detection. report_changes="yes" includes a diff of the file content in the alert. The frequency value (seconds) is for periodic full rescans.
11. Reverse Proxy (Pangolin / Traefik)
Section titled “11. Reverse Proxy (Pangolin / Traefik)”The dashboard is already HTTPS with a self-signed cert. To front it with your reverse proxy you need to tell Traefik to not verify the backend cert:
http: services: wazuh: loadBalancer: servers: - url: https://wazuh-dashboard:443 serversTransport: insecure-backend
serversTransports: insecure-backend: insecureSkipVerify: truePangolin handles this automatically when you mark the target as HTTPS in the resource config.
12. Routine Operations
Section titled “12. Routine Operations”Stats.log keeps growing. Suricata’s stats.log is unbounded. Truncate it on a cron or lower the stats frequency in suricata.yaml (default 8s is noisy for a homelab).
Update Suricata rules. The image’s entrypoint calls suricata-update on every start. Restart Suricata weekly to pull fresh rules: docker compose restart suricata.
Update Wazuh. Pin the image tag (4.14.3 in the compose) and bump deliberately. Minor releases between Wazuh versions change index templates; a blind latest can break ingestion.
Backups. Everything stateful is in Docker volumes (listed at the bottom of the compose) + the NFS cert dir. docker run --rm -v wazuh_etc:/data -v $PWD:/backup alpine tar czf /backup/wazuh_etc.tgz -C /data . per volume, or include them in your borgmatic config.
13. Troubleshooting
Section titled “13. Troubleshooting”Dashboard loads but shows “wazuh API not responding”. Manager is not up yet, or the dashboard’s API_PASSWORD doesn’t match the manager’s. Check docker logs wazuh-dashboard.
Manager logs say “filebeat: SSL handshake failed”. Cert SAN mismatch — set FILEBEAT_SSL_VERIFICATION_MODE=certificate (not full). Regenerate certs if you changed node IPs.
“No data” in any Wazuh dashboard tab. Go to Indexer management → Index patterns and confirm wazuh-alerts-* exists. If not, the manager isn’t shipping to the indexer — check docker logs wazuh-manager | grep filebeat.
Suricata alerts not appearing in Wazuh. Verify the mount (docker exec wazuh-manager ls -la /var/log/suricata/eve.json), verify ossec.conf has the <localfile> entry, and that the manager restarted after the edit.
Redirect loop in the dashboard. multitenancy setting mismatch between indexer and dashboard configs. Both must be true (or both false).
Can’t log in as admin. Wazuh 4.14.3’s INDEXER_PASSWORD env is bootstrap only. The live password is in internal_users.yml — default admin. Use the wazuh-passwords-tool.sh to rotate it properly.