Integrating n8n with Wazuh (Custom Integration)
2026-09-17 · Neetrox
This guide wires the Wazuh manager's integrator daemon to an n8n webhook, so every alert above a chosen severity is POSTed to n8n as JSON for SOAR-style automation.
Three files are involved:

Naming rule: the script name must start with
custom-, and the shell wrapper and the Python file must share the same base name (custom-n8nandcustom-n8n.py). Wazuh resolves the Python file by appending.pyto the executed script name.
1. Prerequisites
Wazuh manager 4.x installed at
/var/ossec(adjust paths if you use/var/ossecvs. a custom prefix).A reachable n8n instance. In the sample config that's
http://192.168.10.15:5678.Network path open from the manager to n8n on the n8n port (default
5678):curl -v http://192.168.10.15:5678/healthzrootaccess on the manager.
2. Install the shell wrapper
Create /var/ossec/integrations/custom-n8n. This is the standard Wazuh integration wrapper (a copy of the Slack one, renamed) — it figures out where the manager is installed and executes the matching .py file with Wazuh's bundled Python 3, so you don't depend on the system Python or its site-packages.
#!/bin/sh
# Copyright (C) 2015, Wazuh Inc.
# Created by Wazuh, Inc. <info@wazuh.com>.
# This program is free software; you can redistribute it and/or modify it under the terms of GPLv2
# this is a copy of the slack integration file renamed to custom-n8n
WPYTHON_BIN="framework/python/bin/python3"
SCRIPT_PATH_NAME="$0"
DIR_NAME="$(cd $(dirname ${SCRIPT_PATH_NAME}); pwd -P)"
SCRIPT_NAME="$(basename ${SCRIPT_PATH_NAME})"
case ${DIR_NAME} in
*/active-response/bin | */wodles*)
if [ -z "${WAZUH_PATH}" ]; then
WAZUH_PATH="$(cd ${DIR_NAME}/../..; pwd)"
fi
PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py"
;;
*/bin)
if [ -z "${WAZUH_PATH}" ]; then
WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)"
fi
PYTHON_SCRIPT="${WAZUH_PATH}/framework/scripts/$(echo ${SCRIPT_NAME} | sed 's/\-/_/g').py"
;;
*/integrations)
if [ -z "${WAZUH_PATH}" ]; then
WAZUH_PATH="$(cd ${DIR_NAME}/..; pwd)"
fi
PYTHON_SCRIPT="${DIR_NAME}/${SCRIPT_NAME}.py"
;;
esac
${WAZUH_PATH}/${WPYTHON_BIN} ${PYTHON_SCRIPT} "$@"
3. Install the Python sender
Create /var/ossec/integrations/custom-n8n.py. integratord calls it with positional arguments: argv[1] = path to the temporary alert JSON file, argv[2] = API key, argv[3] = hook URL, argv[4] = options. That is why the hook URL is read from sys.argv[3].
#!/usr/bin/env python3
import sys
import json
import requests
from requests.exceptions import RequestException
# Read configuration
alert_file = open(sys.argv[1])
hook_url = sys.argv[3]
# Read the full alert
alert_json = json.loads(alert_file.read())
alert_file.close()
# Extract key information for easy access
rule = alert_json.get("rule", {})
agent = alert_json.get("agent", {})
data = alert_json.get("data", {})
# Send the COMPLETE alert to n8n
payload = {
"full_alert": alert_json, # Complete original alert
"summary": {
"rule_id": rule.get("id"),
"rule_level": rule.get("level"),
"rule_description": rule.get("description"),
"agent_name": agent.get("name"),
"agent_id": agent.get("id"),
"timestamp": alert_json.get("timestamp"),
"src_ip": data.get("srcip"),
"full_log": alert_json.get("full_log")
}
}
# Send to n8n webhook
try:
response = requests.post(
hook_url,
json=payload,
headers={"Content-Type": "application/json"},
timeout=10
)
if response.status_code == 200:
sys.exit(0)
else:
sys.exit(1)
except RequestException as e:
sys.exit(1)
The payload has two halves: full_alert keeps the untouched Wazuh alert for anything you need later, and summary gives your n8n nodes flat, predictable fields ({{ $json.summary.src_ip }}) so you're not writing deep expressions in every node.
4. Set ownership and permissions
integratord refuses to run a script that isn't owned correctly or isn't executable.
chown root:wazuh /var/ossec/integrations/custom-n8n /var/ossec/integrations/custom-n8n.py
chmod 750 /var/ossec/integrations/custom-n8n /var/ossec/integrations/custom-n8n.py
Verify:
ls -l /var/ossec/integrations/custom-n8n*
# -rwxr-x--- 1 root wazuh ... custom-n8n
# -rwxr-x--- 1 root wazuh ... custom-n8n.py
5. Register the integration in ossec.conf
Add the block inside <ossec_config> in /var/ossec/etc/ossec.conf:
<integration>
<name>custom-n8n</name>
<hook_url>http://192.168.10.15:5678/webhook/rulelvl5</hook_url>
<level>10</level>
<alert_format>json</alert_format>
</integration>
name— must match the script filename exactly.hook_url— the n8n production webhook URL (see step 6).level— minimum rule level; only alerts at or above this level are forwarded.alert_format— must bejson, otherwise the alert file isn't JSON andjson.loads()in the script fails.
Optional filters you can add alongside <level>: <rule_id>, <group>, <event_location>. Multiple filters are ANDed, so start with <level> alone until the pipeline works.
Heads-up on the sample: the webhook path is
rulelvl5but the filter is<level>10</level>. Nothing breaks, but the path name is misleading — either rename the n8n webhook path torulelvl10or set<level>5</level>, so the next person reading the config isn't misled about what's actually being forwarded. Be aware that level 5 is far chattier than level 10.
Validate the config and restart:
/var/ossec/bin/wazuh-logtest -t # sanity check config parsing
systemctl restart wazuh-manager
systemctl status wazuh-manager
Confirm the daemon came up:
/var/ossec/bin/wazuh-control status | grep integrator
# wazuh-integratord is running...
6. Build the receiving workflow in n8n
New workflow → add a Webhook node.
Set HTTP Method to
POST.Set Path to
rulelvl5(must match the tail ofhook_url).Set Respond to Immediately — the Python script times out after 10 seconds, so a long-running workflow that only responds at the end will look like a failure to Wazuh even when it actually ran.
Save and activate the workflow. n8n exposes two URLs per webhook: the Test URL (
/webhook-test/..., live only while you click "Listen for test event") and the Production URL (/webhook/..., live only while the workflow is active).ossec.confmust use the Production URL.Add your downstream nodes — enrichment, Slack/Telegram notification, ticket creation, active response callback, whatever the playbook needs.
Useful expressions once the first alert lands:
{{ $json.summary.rule_level }}
{{ $json.summary.rule_description }}
{{ $json.summary.agent_name }}
{{ $json.summary.src_ip }}
{{ $json.full_alert.rule.mitre.id }}
7. Test end to end
Manual run (fastest way to isolate script problems from Wazuh problems). Save a real alert to a file first — grab one from /var/ossec/logs/alerts/alerts.json:
tail -n 1 /var/ossec/logs/alerts/alerts.json > /tmp/test_alert.json
sudo -u root /var/ossec/integrations/custom-n8n \
/tmp/test_alert.json \
"" \
"http://192.168.10.15:5678/webhook/rulelvl5"
echo "exit code: $?"
Exit code 0 means n8n returned HTTP 200.
Trigger a real alert. An easy level-10 candidate is repeated SSH auth failures from one source (rule 5712, "sshd brute force"):
for i in $(seq 1 10); do ssh invaliduser@<agent-ip>; done
Then watch the manager and n8n:
tail -f /var/ossec/logs/ossec.log | grep -i integrator
In n8n, check Executions for the incoming run.
8. Troubleshooting
Enable integrator debug logging:
echo "integrator.debug=2" >> /var/ossec/etc/local_internal_options.conf
systemctl restart wazuh-manager
tail -f /var/ossec/logs/ossec.log

The script swallows errors silently (both failure branches just sys.exit(1)). While debugging it's worth adding a line to the exception handler:
except RequestException as e:
print(f"n8n integration failed: {e}", file=sys.stderr)
sys.exit(1)
integratord captures stderr into ossec.log, so that one line turns a blind exit code into an actual diagnosis.
9. Hardening notes for production
Use HTTPS. The alert payload contains full log lines — hostnames, usernames, source IPs, sometimes command lines. Plain HTTP over a shared network exposes all of it. Put n8n behind TLS and update
hook_urltohttps://.Authenticate the webhook. Anyone who can reach
:5678can currently inject fake alerts into your automation. Enable Header Auth on the n8n Webhook node and send the matching header from the script, or pass a secret via<api_key>inossec.confand read it fromsys.argv[2].Watch the alert volume. One HTTP POST per alert, synchronously, on the manager. Level 10+ is usually fine; dropping the threshold to 5 on a busy deployment can generate enough traffic to back up the integrator queue.
Don't widen
<level>without a filter. Prefer adding<rule_id>or<group>for specific playbooks rather than lowering the global threshold.Version the scripts. Keep
custom-n8nandcustom-n8n.pyin Git; a manager upgrade can overwrite or orphan files in/var/ossec/integrations/.
Rather not run this yourself?
Everything above is yours to set up and keep. If you would rather hand it over, there are two ways.
Done For You, $129 one-time. We install the integration and your workflow in your environment, connect your credentials, test it end to end and leave you a runbook. Anything that breaks in the first 30 days we fix for free, and if we cannot get it running you get your money back. Buy it here
Managed SOC Automation, $249 a month. Setup is included. After that we watch the workflow runs, failures, credentials and integrations, and respond the next business day when something breaks, including after a Wazuh upgrade moves files in /var/ossec/integrations/. Book a 15-minute call
All four plans are compared on the services page.