About
Most Wazuh-to-notification tutorials stop at "poll a JSON file every few seconds and forward it." This project does the opposite: it wires Wazuh's own Integrator module directly into an n8n workflow over an authenticated production webhook, so alerts move in real time and get validated and deduplicated before anyone sees them.
Alerts are durably queued to disk when the n8n webhook is unreachable at delivery time, and a systemd timer drains that queue automatically once n8n is back — tested by killing n8n mid-flight and confirming automatic recovery.
It was built, broken, and fixed on real infrastructure — including a genuine Windows endpoint agent, a controlled brute-force simulation, an accidental self-inflicted alert, and a real operational issue (a compliance-scan flood) diagnosed and resolved during lab validation. Every claim in this write-up is backed by a matched pair of screenshots: one from the Wazuh dashboard, one from the resulting email, same rule ID, same timestamp.
Architecture
| Stage | Component | Responsibility |
|---|---|---|
| 1 | Monitored host | Linux manager + Windows endpoint agent generate raw events: auth logs, sudo activity, file integrity changes, compliance checks |
| 2 | Wazuh Manager | Decodes and classifies events, assigns a rule ID and severity level |
| 3 | Wazuh Integrator | Custom script fires on level ≥ 7, authenticates with a secret header token, posts to the webhook — event-driven, not a polling loop |
| 4 | n8n Webhook | Header-token authenticated ingress, responds 202 immediately after auth, then hands off to processing |
| 5 | Normalize | Flattens the raw alert into a consistent shape, maps rule level to a severity label |
| 6 | Validate | Rejects malformed or incomplete payloads before they propagate |
| 7 | Deduplicate | Tracks event IDs already processed; repeats are dropped silently |
| 8 | Severity Router | Branches on severity label; low-severity and explicitly excluded noisy sources never generate an email |
| 9 | Format | Builds an HTML report; the subject line alone is enough to triage |
| 10 | Gmail Delivery | Sends via an authenticated OAuth2 account under a dedicated sender identity |
If the webhook is unreachable at step 4, the alert is written to a local disk queue instead of being dropped. A systemd timer retries every 60 seconds and drains the queue automatically the moment n8n comes back.
Test cases — every claim backed by a matched pair of screenshots
Each case below pairs the Wazuh-side detection with the resulting notification — same rule ID, same timestamp, proving the pipeline end to end rather than a mocked demo.
Case 1 — Controlled brute-force detection
A controlled SSH brute-force simulation against a non-existent user, run inside the authorized local lab, was detected natively by Wazuh (rule 5712, level 10) and forwarded automatically, with no manual intervention.


Case 2 — Deduplication verified
The same event ID was submitted twice in a row. The first execution ran the full chain to Gmail; the second was cleanly filtered at the deduplication step and never reached the inbox — one incident, one notification.


Case 3 — An accidental, fully organic alert
While debugging an SSH key permission issue, three consecutive sudo password typos triggered a genuine Wazuh detection (rule 5404, "Three failed attempts to run sudo") with zero staging. It reached the inbox in real time.


Case 4 — Real Windows endpoint, and a lesson in tuning
A genuine Wazuh agent was deployed on a Windows 11 host. Its Security Configuration Assessment module ran a full CIS benchmark scan — 482 checks, 350 failing, many at level 7+. Because deduplication keys on event ID and every check has a unique one, every failed check produced its own independent, valid alert — a real flood.
Deduplication by event ID is correct for suppressing a retransmitted copy of the same incident, but it can't collapse a batch of genuinely distinct sub-events from one noisy source — that filtering has to happen upstream. The pipeline behaved exactly as configured; the volume exposed a missing scope filter on a source that was never meant to page anyone.


Immediate action: stop the flood at the source. Durable fix: exclude the sca rule group from the real-time forwarding path while leaving it visible in Wazuh Dashboard for periodic review — implemented in wazuh/custom-n8n (EXCLUDED_RULE_GROUPS).
Lessons learned
- Event-driven beats polling. Wiring the Integrator directly kept observed alert latency in the low single-digit seconds across every test case.
- Deduplication needs a scope, not just a key. Keying on event ID alone is correct for genuine incidents, but a noisy source that mints a fresh ID per sub-check bypasses it entirely — the fix belongs at the source filter, not the dedup logic.
- A downtime test is only real if you kill the service mid-flight. Stopping the container and watching the disk queue drain automatically on restart proves recovery; simulating it in code doesn't.
- The best failure mode is a loud one. The compliance-scan flood was diagnosed and root-caused within minutes because the pipeline surfaced every alert individually instead of swallowing errors silently.
Security controls
- The n8n production webhook requires header-token authentication — not a token embedded in the URL.
- The Wazuh Integrator script reads the token from a root-owned, group-restricted file (
640,root:wazuh); never hardcoded or logged. - Gmail delivery uses OAuth2 — no application password stored in plaintext.
- n8n listens on
127.0.0.1only; remote access goes through an SSH tunnel, never direct exposure. - Host firewall is default-deny with explicit allow rules scoped to required ports.
- Secrets, tokens, and credential IDs are excluded from the repository; screenshots were reviewed before publication — only private RFC1918 lab addresses are visible.