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:

wazuh n8n integration

Naming rule: the script name must start with custom-, and the shell wrapper and the Python file must share the same base name (custom-n8n and custom-n8n.py). Wazuh resolves the Python file by appending .py to the executed script name.


1. Prerequisites

  • Wazuh manager 4.x installed at /var/ossec (adjust paths if you use /var/ossec vs. 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/healthz
    
  • root access 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 be json, otherwise the alert file isn't JSON and json.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 rulelvl5 but the filter is <level>10</level>. Nothing breaks, but the path name is misleading — either rename the n8n webhook path to rulelvl10 or 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

  1. New workflow → add a Webhook node.

  2. Set HTTP Method to POST.

  3. Set Path to rulelvl5 (must match the tail of hook_url).

  4. 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.

  5. 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.conf must use the Production URL.

  6. 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
wazuh and n8n integration steps

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_url to https://.

  • Authenticate the webhook. Anyone who can reach :5678 can 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> in ossec.conf and read it from sys.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-n8n and custom-n8n.py in 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.

← Back to blog