Fail2Ban SSH Ban Integration with n8n Webhooks

{"time":1776378046142,"version":"2.28.2","blocks":[{"type":"paragraph","data":{"text":"Document Owner: IT / Network"}},{"type":"paragraph","data":{"text":"Scope: Linux servers running OpenSSH where Fail2Ban enforces bans and notifies n8n via webhook."}},{"type":"paragraph","data":{"text":"Goal: Permanently ban brute-force SSH IPs locally (bantime = -1) and send events to n8n for enrichment and alerting."}},{"type":"header","data":{"text":"Table of Contents","level":3}},{"type":"list","data":{"style":"ordered","items":["How It Works","Architecture","Prerequisites","SOP 1 — Install Fail2Ban","SOP 2 — Create n8n Webhook Action","SOP 3 — Configure SSH Jail (Permanent Ban + Multi-Action)","SOP 4 — Validate and Test","SOP 5 — Operations (Monitoring and Health Checks)","SOP 6 — Manual Unban","SOP 7 — Incident Recovery (Accidental Self-Ban)","SOP 8 — Secure the Webhook (Production Standard)","SOP 9 — Change Control"]}},{"type":"header","data":{"text":"How It Works","level":3}},{"type":"paragraph","data":{"text":"Every SSH server on the internet faces constant brute-force login attempts. Fail2Ban monitors your SSH log files for repeated authentication failures and automatically blocks offending IP addresses at the firewall level. In this configuration, bans are permanent (bantime = -1), meaning an attacker does not get a second chance."}},{"type":"paragraph","data":{"text":"On its own, Fail2Ban only protects the individual server it runs on. This setup extends it by adding a webhook action that sends ban and unban events to an n8n workflow. n8n acts as the centralized brain — it can enrich each event with threat intelligence (such as IP geolocation or abuse reports), send notifications to Slack or email, and correlate attacks across multiple servers. The result is a two-layer system: Fail2Ban handles real-time local enforcement, and n8n handles visibility and alerting across your entire fleet."}},{"type":"paragraph","data":{"text":"The webhook is designed to be failure-safe. If n8n is unreachable, the curl command times out after 8 seconds and exits silently. The firewall ban still applies regardless. Enforcement never depends on the webhook succeeding."}},{"type":"header","data":{"text":"Architecture","level":3}},{"type":"paragraph","data":{"text":"Fail2Ban serves as the local enforcement layer, while n8n provides centralized alerting and intelligence. The data flows as follows:"}},{"type":"list","data":{"style":"unordered","items":["sshd writes authentication failures to /var/log/auth.log (or journald, depending on your distribution).","Fail2Ban detects brute-force patterns based on the jail’s maxretry and findtime thresholds, then applies a local firewall ban.","n8n receives webhook events and can enrich them with IP intelligence, send notifications to Slack, and correlate bans across multiple servers."]}},{"type":"code","data":{"code":"sshd logs → Fail2Ban jail → firewall ban (%(action_)s)\n ↘︎ webhook notify → n8n workflow"}},{"type":"header","data":{"text":"Prerequisites","level":3}},{"type":"paragraph","data":{"text":"Before starting, confirm the following are in place:"}},{"type":"list","data":{"style":"unordered","items":["Ubuntu/Debian server (or a compatible distribution)","OpenSSH installed and running","SSH authentication log available at /var/log/auth.log","Outbound HTTPS traffic allowed to your n8n domain","An n8n webhook endpoint created and set to accept POST requests"]}},{"type":"header","data":{"text":"SOP 1 — Install Fail2Ban","level":3}},{"type":"header","data":{"text":"Procedure","level":4}},{"type":"list","data":{"style":"ordered","items":["Install the package and enable the service:"]}},{"type":"code","data":{"code":"sudo apt update\nsudo apt install -y fail2ban\nsudo systemctl enable --now fail2ban"}},{"type":"header","data":{"text":"Validation","level":4}},{"type":"paragraph","data":{"text":"Confirm that Fail2Ban is running and responsive:"}},{"type":"code","data":{"code":"systemctl is-active fail2ban\nsudo fail2ban-client ping"}},{"type":"paragraph","data":{"text":"You should see active and pong in the output, respectively."}},{"type":"header","data":{"text":"SOP 2 — Create n8n Webhook Action","level":3}},{"type":"paragraph","data":{"text":"This step creates a custom Fail2Ban action that sends a webhook to n8n whenever an IP is banned or unbanned. The action does not replace the firewall ban — it runs alongside it."}},{"type":"header","data":{"text":"Procedure","level":4}},{"type":"list","data":{"style":"ordered","items":["Create the action definition file:"]}},{"type":"code","data":{"code":"sudo nano /etc/fail2ban/action.d/n8n-webhook.conf"}},{"type":"list","data":{"style":"ordered","items":["Paste the following configuration:"]}},{"type":"code","data":{"code":"[Definition]\n\n# NOTE:\n# - actionban/actionunban are all we need for webhook notifications.\n# - actionstart/actionstop/actioncheck are intentionally omitted\n# because webhooks do not require lifecycle setup.\n\nactionban = curl -sS -m 8 -X POST \"\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"event\":\"fail2ban_ban\",\"jail\":\"\",\"ip\":\"\",\"fq_hostname\":\"\",\"failures\":\"\",\"time\":\"\",\"token\":\"\"}' \\\n >/dev/null 2>&1 || true\n\nactionunban = curl -sS -m 8 -X POST \"\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"event\":\"fail2ban_unban\",\"jail\":\"\",\"ip\":\"\",\"fq_hostname\":\"\",\"time\":\"\",\"token\":\"\"}' \\\n >/dev/null 2>&1 || true\n\n[Init]\nn8n_url =\ntoken =\nfq_hostname ="}},{"type":"header","data":{"text":"Design Notes","level":4}},{"type":"list","data":{"style":"unordered","items":["No actionstart/stop/check: Webhooks do not require lifecycle hooks. Only ban and unban events matter for alerting.","Timeout (-m 8): Limits the curl request to 8 seconds so Fail2Ban does not hang if n8n is slow or unreachable.","Failure safety (|| true): Ensures that a webhook failure never prevents the firewall ban from being applied. The ban always succeeds, even if the notification does not."]}},{"type":"header","data":{"text":"SOP 3 — Configure SSH Jail (Permanent Ban + Multi-Action)","level":3}},{"type":"paragraph","data":{"text":"This jail tells Fail2Ban to watch for SSH brute-force attempts, permanently ban offending IPs, and notify n8n at the same time."}},{"type":"header","data":{"text":"Procedure","level":4}},{"type":"list","data":{"style":"ordered","items":["Edit the jail configuration:"]}},{"type":"code","data":{"code":"sudo nano /etc/fail2ban/jail.local"}},{"type":"list","data":{"style":"ordered","items":["Add or replace the SSH jail with the following:"]}},{"type":"code","data":{"code":"[sshd]\nenabled = true\nport = 22\nfilter = sshd\nlogpath = /var/log/auth.log\nbackend = auto\n\nmaxretry = 5\nfindtime = 600\nbantime = -1\n\n# Multi-action:\n# 1) %(action_)s = default firewall ban action\n# 2) n8n-webhook = notify n8n\naction = %(action_)s\n n8n-webhook[n8n_url=\"https://YOUR_N8N_DOMAIN/webhook/fail2ban\", token=\"YOUR_TOKEN\", fq_hostname=\"YOURSERVER.example.com\"]"}},{"type":"paragraph","data":{"text":"Replace the three placeholder values with your actual n8n webhook URL, authentication token, and server hostname."}},{"type":"header","data":{"text":"Key Settings Explained","level":4}},{"type":"list","data":{"style":"unordered","items":["maxretry = 5: An IP is banned after 5 failed login attempts.","findtime = 600: Those 5 failures must occur within 600 seconds (10 minutes).","bantime = -1: Bans are permanent. The IP stays blocked until manually unbanned (see SOP 6).","Multi-action: Two actions run on every ban — the default firewall rule and the n8n webhook notification."]}},{"type":"header","data":{"text":"Critical Token Warning","level":4}},{"type":"paragraph","data":{"text":"Avoid using the # character in your token. Fail2Ban’s configuration parser treats # as the start of a comment, which will silently truncate your token value."}},{"type":"list","data":{"style":"unordered","items":["Safe: SecureToken_ABC123","If you must use #: Escape it with a backslash (e.g., abc\\#123)"]}},{"type":"header","data":{"text":"Apply the Changes","level":4}},{"type":"code","data":{"code":"sudo systemctl restart fail2ban"}},{"type":"header","data":{"text":"SOP 4 — Validate and Test","level":3}},{"type":"header","data":{"text":"Check the SSH jail status","level":4}},{"type":"code","data":{"code":"sudo fail2ban-client status sshd"}},{"type":"paragraph","data":{"text":"You should see the jail listed as active with the correct filter and action count."}},{"type":"header","data":{"text":"Review Fail2Ban logs","level":4}},{"type":"code","data":{"code":"sudo tail -n 200 /var/log/fail2ban.log"}},{"type":"paragraph","data":{"text":"Look for any configuration errors or warnings. A clean startup means the jail loaded correctly."}},{"type":"header","data":{"text":"Trigger a test ban","level":4}},{"type":"paragraph","data":{"text":"To test the full pipeline without locking yourself out:"}},{"type":"list","data":{"style":"ordered","items":["Use a separate source IP — not your admin IP.","Attempt several failed SSH logins from that IP to exceed the maxretry threshold.","Confirm the ban appears in fail2ban-client status sshd."]}},{"type":"header","data":{"text":"Verify the n8n webhook","level":4}},{"type":"list","data":{"style":"unordered","items":["Open your n8n workflow and confirm the webhook executed.","Check that the payload includes event, ip, fq_hostname, and jail."]}},{"type":"header","data":{"text":"SOP 5 — Operations (Monitoring and Health Checks)","level":3}},{"type":"header","data":{"text":"Daily health checks","level":4}},{"type":"paragraph","data":{"text":"Run the following commands to verify that Fail2Ban is active and the SSH jail is functioning:"}},{"type":"code","data":{"code":"systemctl is-active fail2ban\nsudo fail2ban-client status sshd"}},{"type":"header","data":{"text":"Investigate suspicious spikes","level":4}},{"type":"paragraph","data":{"text":"If you notice a sudden increase in bans, review the most recent activity:"}},{"type":"code","data":{"code":"sudo tail -n 200 /var/log/fail2ban.log\nsudo grep \"Ban\" /var/log/fail2ban.log | tail -n 50"}},{"type":"header","data":{"text":"Expected behavior","level":4}},{"type":"list","data":{"style":"unordered","items":["Fail2Ban remains active across reboots.","Banned IPs accumulate gradually over time.","No repeated curl or webhook failures appear in the logs."]}},{"type":"header","data":{"text":"SOP 6 — Manual Unban","level":3}},{"type":"paragraph","data":{"text":"Because bantime = -1 makes bans permanent, you will need to unban IPs manually when necessary."}},{"type":"header","data":{"text":"List currently banned IPs","level":4}},{"type":"code","data":{"code":"sudo fail2ban-client status sshd"}},{"type":"header","data":{"text":"Unban a specific IP","level":4}},{"type":"code","data":{"code":"sudo fail2ban-client set sshd unbanip 1.2.3.4"}},{"type":"paragraph","data":{"text":"Replace 1.2.3.4 with the actual IP address you want to unban. This also triggers the actionunban webhook, so n8n will be notified of the unban."}},{"type":"header","data":{"text":"SOP 7 — Incident Recovery (Accidental Self-Ban)","level":3}},{"type":"paragraph","data":{"text":"If you accidentally lock yourself out by triggering the ban threshold from your own IP, follow these steps."}},{"type":"header","data":{"text":"Recovery steps","level":4}},{"type":"list","data":{"style":"ordered","items":["Access the server through an alternative method — cloud provider console, physical access, or out-of-band management.","Unban your IP:"]}},{"type":"code","data":{"code":"sudo fail2ban-client set sshd unbanip YOUR.PUBLIC.IP.ADDRESS"}},{"type":"header","data":{"text":"Prevention: Whitelist your admin IP","level":4}},{"type":"paragraph","data":{"text":"If your admin IP address is static, you can prevent accidental self-bans by adding it to the ignoreip list in your jail configuration:"}},{"type":"code","data":{"code":"[sshd]\nignoreip = 127.0.0.1/8 YOUR.PUBLIC.IP.ADDRESS"}},{"type":"paragraph","data":{"text":"Do not add dynamic IPs to ignoreip — if your IP changes, the old address would remain whitelisted and could be reassigned to someone else."}},{"type":"header","data":{"text":"SOP 8 — Secure the Webhook (Production Standard)","level":3}},{"type":"paragraph","data":{"text":"The webhook endpoint should be hardened before deploying to production. At minimum, implement the following:"}},{"type":"list","data":{"style":"unordered","items":["Validate the token: The n8n workflow should check the token field as its first step and reject requests with an invalid or missing token by returning a 401 or 403 response.","Rate-limit the endpoint: Apply rate limiting at your reverse proxy (Nginx, Traefik, or Cloudflare) to prevent abuse.","Restrict source IPs: Optionally, limit inbound access to the webhook URL to only the IP addresses of your servers."]}},{"type":"header","data":{"text":"SOP 9 — Change Control","level":3}},{"type":"paragraph","data":{"text":"Always follow this process when modifying the Fail2Ban configuration."}},{"type":"header","data":{"text":"Before making changes","level":4}},{"type":"paragraph","data":{"text":"Create a timestamped backup of the entire configuration directory:"}},{"type":"code","data":{"code":"sudo cp -a /etc/fail2ban /etc/fail2ban.bak.$(date +%F)"}},{"type":"header","data":{"text":"After making changes","level":4}},{"type":"paragraph","data":{"text":"Restart the service and verify that the jail is still healthy:"}},{"type":"code","data":{"code":"sudo systemctl restart fail2ban\nsudo fail2ban-client status sshd\nsudo tail -n 100 /var/log/fail2ban.log"}},{"type":"paragraph","data":{"text":"Document every change: Record the date and time, what was changed, why it was changed, and the expected impact. Keep this record alongside your configuration backups."}},{"type":"paragraph","data":{"text":"End of SOP."}}]}