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

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_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/.

← Back to blog