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:
File Location on the manager Role custom-n8n /var/ossec/integrations/ Shell wrapper that locates Wazuh's embedded Python and calls the .py script custom-n8n.py /var/ossec/integrations/ Builds the payload and POSTs it to the n8n webhook <integration> block /var/ossec/etc/ossec.conf Tells wazuh-integratord when to fire and where to send
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
Symptom Likely cause Nothing in ossec.log about the integration <integration> block outside <ossec_config>, or name doesn't match the filename Unable to run integration for custom-n8n Wrong ownership/permissions, or the wrapper isn't executable IndexError: list index out of range Script invoked without all positional args — print sys.argv to see what arrived JSONDecodeError <alert_format> isn't json Script exits 1, n8n shows no execution Workflow not activated, wrong path, or wrong URL (test vs. production) Script exits 1, n8n did execute Workflow responds at the end and exceeded the 10 s timeout, or the webhook returned a non-200 Connection refused / timeout Firewall between manager and n8n, or n8n listening only on localhost No alerts arriving at all Nothing is actually hitting level 10 — check with grep '"level":1[0-9]' /var/ossec/logs/alerts/alerts.json
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/.