Fail2Ban is an intrusion prevention framework that monitors log files for suspicious activity and automatically bans offending IP addresses by updating firewall rules. It ships with filters for SSH, Apache, Nginx, Postfix, Dovecot, and many other services.
By adding a custom action, you can make Fail2Ban report every banned IP to the IPWhois Blacklist. This means your server actively contributes to community threat intelligence -- every brute-force attacker, spammer, or scanner that hits your server gets shared with thousands of other sysadmins who use the blacklist to protect their own infrastructure.
The integration works by calling the IPWhois Blacklist report API via curl each time Fail2Ban bans an IP. It runs alongside your existing ban actions (iptables, firewalld, etc.) so your server remains protected while also reporting the threat.
- Fail2Ban 0.10+ installed and running (0.11+ recommended)
- curl installed on the server (used to call the API)
- Root or sudo access to create action files and edit jail configuration
- Outbound HTTPS access to
bl.ipwhois.neton port 443
If Fail2Ban is already installed, skip to Step 1. Otherwise, install it for your distribution:
Ubuntu 20.04 / 22.04 / 24.04
sudo apt update
sudo apt install -y fail2ban curl
# Enable and start the service
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
# Verify it is running
sudo systemctl status fail2ban
nftables backend by default. The IPWhois action works identically regardless of the firewall backend.Debian 11 (Bullseye) / Debian 12 (Bookworm)
sudo apt update
sudo apt install -y fail2ban curl
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
On Debian 12 with the default nftables backend, ensure the nftables service is running:
sudo systemctl enable nftables
sudo systemctl start nftables
CentOS / RHEL / Rocky Linux / AlmaLinux
# Enable EPEL repository (required for Fail2Ban)
sudo dnf install -y epel-release # RHEL/CentOS/Rocky/Alma 8+
# or: sudo yum install -y epel-release # CentOS 7
sudo dnf install -y fail2ban fail2ban-firewalld curl
# or: sudo yum install -y fail2ban fail2ban-firewalld curl
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
fail2ban-firewalld package configures Fail2Ban to use firewalld as the ban action backend. The IPWhois reporting action runs alongside this.Verify your installation:
fail2ban-client version
# Expected output: Fail2Ban v0.11.x or v1.x
fail2ban-client status
# Should show "Number of jail" and list active jails
Fail2Ban actions are defined in /etc/fail2ban/action.d/. Create a new file called ipwhois-report.conf:
sudo nano /etc/fail2ban/action.d/ipwhois-report.conf
Paste the following content:
# IPWhois Blacklist reporter for Fail2Ban
# Reports banned IPs to https://bl.ipwhois.net/
#
# Usage: add "ipwhois-report" to your jail's action list
# Docs: https://bl.ipwhois.net/docs/fail2ban
[Definition]
# actionstart - called when the jail starts (optional logging)
actionstart =
# actionstop - called when the jail stops (optional logging)
actionstop =
# actioncheck - called before each ban to verify prerequisites
actioncheck =
# actionban - called each time an IP is banned
# <ip> = the banned IP address (injected by Fail2Ban)
# <name> = the jail name (e.g., sshd, postfix, nginx-http-auth)
actionban = /usr/bin/curl -sSf -m 10 -X POST https://bl.ipwhois.net/api/report \
-d "ip=<ip>" \
-d "type=%(threat_type)s" \
-d "message=Fail2Ban+jail+<name>+on+%(hostname)s" \
2>&1 | logger -t ipwhois-bl
# actionunban - called when the ban expires (nothing to do)
actionunban =
[Init]
# Default threat type - override per jail in jail.local
threat_type = brute-force
# Server hostname included in the report message
hostname = %(hostname)s
Line-by-line explanation
| Line / Directive | Purpose |
|---|---|
actionban | The command Fail2Ban executes every time it bans an IP. This is where the API call happens. |
/usr/bin/curl | Full path to curl. Using the absolute path avoids issues when Fail2Ban runs in a restricted environment. |
-sSf | -s silent, -S show errors, -f fail on HTTP errors. Prevents noisy output while still catching real failures. |
-m 10 | 10-second timeout. Prevents Fail2Ban from hanging if the API is slow. |
-X POST | Sends an HTTP POST request to the report endpoint. |
-d "ip=<ip>" | <ip> is a Fail2Ban variable that gets replaced with the actual banned IP address. |
-d "type=%(threat_type)s" | The threat category. Defaults to brute-force but can be overridden per jail. |
-d "message=..." | Human-readable context including the jail name and server hostname. |
2>&1 | logger -t ipwhois-bl | Redirects output to syslog with tag ipwhois-bl for easy log filtering. |
actionunban | Left empty. There is no need to "un-report" an IP from the blacklist. |
[Init] threat_type | Default threat type. Override this in jail.local per jail section. |
sudo chmod 644 /etc/fail2ban/action.d/ipwhois-report.confEdit your jail.local file to add the IPWhois reporting action alongside your existing ban action. Never edit jail.conf directly as it gets overwritten on package upgrades.
sudo nano /etc/fail2ban/jail.local
Add or modify the [sshd] section:
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
findtime = 600
bantime = 3600
# Ban with firewall AND report to IPWhois Blacklist
action = iptables-multiport[name=sshd, port="ssh", protocol=tcp]
ipwhois-report[threat_type=brute-force]
Key points
- The
actiondirective takes multiple actions separated by newlines (indented). The first action (iptables-multiport) blocks the IP in the firewall. The second action (ipwhois-report) reports it to the blacklist. threat_type=brute-forcetells the API this is a brute-force attack. See the threat types table below for other values.- On systems using firewalld (CentOS/RHEL), replace
iptables-multiportwithfirewallcmd-ipset. - On systems using nftables (Debian 12, Ubuntu 24.04), replace with
nftables-multiport.
/var/log/secure instead of /var/log/auth.log. Also use firewallcmd-ipset as the firewall action.[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/secure
maxretry = 5
findtime = 600
bantime = 3600
action = firewallcmd-ipset[name=sshd, port="ssh", protocol=tcp]
ipwhois-report[threat_type=brute-force]
After saving, restart Fail2Ban:
sudo systemctl restart fail2ban
# Verify the jail is active
sudo fail2ban-client status sshd
Expected output:
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 47
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 1
|- Total banned: 12
`- Banned IP list: 203.0.113.42
The same ipwhois-report action works with any Fail2Ban jail. Here are configurations for common services:
Postfix (email spam)
[postfix]
enabled = true
port = smtp,465,submission
filter = postfix[mode=auth]
logpath = /var/log/mail.log
maxretry = 3
bantime = 7200
action = iptables-multiport[name=postfix, port="smtp,465,submission"]
ipwhois-report[threat_type=spam]
Dovecot (IMAP/POP3 brute-force)
[dovecot]
enabled = true
port = pop3,pop3s,imap,imaps
filter = dovecot
logpath = /var/log/mail.log
maxretry = 5
bantime = 3600
action = iptables-multiport[name=dovecot, port="pop3,pop3s,imap,imaps"]
ipwhois-report[threat_type=brute-force]
Apache (HTTP auth brute-force)
[apache-auth]
enabled = true
port = http,https
filter = apache-auth
logpath = /var/log/apache2/error.log
maxretry = 5
bantime = 3600
action = iptables-multiport[name=apache, port="http,https"]
ipwhois-report[threat_type=brute-force]
[apache-badbots]
enabled = true
port = http,https
filter = apache-badbots
logpath = /var/log/apache2/access.log
maxretry = 2
bantime = 86400
action = iptables-multiport[name=apache-badbots, port="http,https"]
ipwhois-report[threat_type=bot]
[apache-noscript]
enabled = true
port = http,https
filter = apache-noscript
logpath = /var/log/apache2/error.log
maxretry = 5
bantime = 3600
action = iptables-multiport[name=apache-noscript, port="http,https"]
ipwhois-report[threat_type=scan]
Nginx (HTTP auth + bot detection)
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5
bantime = 3600
action = iptables-multiport[name=nginx-auth, port="http,https"]
ipwhois-report[threat_type=brute-force]
[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/access.log
maxretry = 3
bantime = 86400
action = iptables-multiport[name=nginx-bot, port="http,https"]
ipwhois-report[threat_type=scan]
WordPress (wp-login brute-force)
Requires a custom filter. Create /etc/fail2ban/filter.d/wordpress-login.conf:
[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php
^<HOST> .* "POST /xmlrpc\.php
ignoreregex =
Then add the jail:
[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 3600
action = iptables-multiport[name=wordpress, port="http,https"]
ipwhois-report[threat_type=brute-force]
After adding any new jails, always restart Fail2Ban:
sudo systemctl restart fail2ban
sudo fail2ban-client status
Use the correct threat_type parameter for each jail so the blacklist categorizes reports accurately:
| Jail | threat_type | Description |
|---|---|---|
sshd | brute-force | SSH login attempts |
postfix, postfix-sasl | spam | Email relay / SMTP auth abuse |
dovecot | brute-force | IMAP/POP3 login attempts |
apache-auth, nginx-http-auth | brute-force | HTTP authentication attempts |
apache-badbots | bot | Malicious crawler / scraper |
apache-noscript, nginx-botsearch | scan | Vulnerability scanning / path probing |
wordpress-login | brute-force | WordPress login brute-force |
recidive | brute-force | Repeat offenders banned by multiple jails |
| DDoS-related jails | ddos | Flood / volumetric attacks |
| Custom phishing jails | phishing | Phishing page hosting or credential theft |
| Malware C&C jails | malware | Known malware command-and-control |
1. Test the API call manually
Before relying on Fail2Ban, verify the API is reachable from your server:
curl -sSf -X POST https://bl.ipwhois.net/api/report \
-d "ip=192.0.2.1" \
-d "type=brute-force" \
-d "message=Test+report+from+setup"
Expected response (JSON):
{"success":true,"message":"Report received"}
2. Trigger a test ban
Manually ban a test IP to see the full flow:
# Ban a test IP (use a documentation/test range IP)
sudo fail2ban-client set sshd banip 192.0.2.99
# Check it was banned
sudo fail2ban-client status sshd
# Unban it when done testing
sudo fail2ban-client set sshd unbanip 192.0.2.99
3. Check syslog for the report
The action pipes output to syslog with the tag ipwhois-bl:
# View recent reports
sudo grep "ipwhois-bl" /var/log/syslog | tail -10
# Or on CentOS/RHEL:
sudo grep "ipwhois-bl" /var/log/messages | tail -10
# Follow in real time
sudo tail -f /var/log/syslog | grep "ipwhois-bl"
4. Verify on bl.ipwhois.net
After a report, you can check the IP on the blacklist:
# Check via API
curl -s "https://bl.ipwhois.net/api/check?ip=192.0.2.99" | python3 -m json.tool
# Or visit in your browser:
# https://bl.ipwhois.net/check/192.0.2.99
5. Check Fail2Ban logs
# Fail2Ban's own log
sudo tail -50 /var/log/fail2ban.log
# Look for ban/unban entries
sudo grep "Ban\|Unban" /var/log/fail2ban.log | tail -20
curl: command not found
The action uses /usr/bin/curl. If curl is not installed:
# Debian/Ubuntu
sudo apt install -y curl
# CentOS/RHEL
sudo dnf install -y curl
# Verify the path
which curl
# Should output: /usr/bin/curl
If curl is installed at a different path (e.g., /usr/local/bin/curl), update the path in the action file or create a symlink:
sudo ln -s $(which curl) /usr/bin/curl
Permission denied
If you see permission errors in syslog, check file ownership:
# Action file should be readable
ls -la /etc/fail2ban/action.d/ipwhois-report.conf
# Expected: -rw-r--r-- root root
# Fix if needed
sudo chown root:root /etc/fail2ban/action.d/ipwhois-report.conf
sudo chmod 644 /etc/fail2ban/action.d/ipwhois-report.conf
API unreachable / connection timeout
If reports are not going through:
# Test connectivity
curl -v https://bl.ipwhois.net/api/check?ip=8.8.8.8
# Check DNS resolution
nslookup bl.ipwhois.net
# Check if outbound HTTPS is blocked by firewall
sudo iptables -L OUTPUT -n | grep 443
# If behind a proxy, set the environment variable in the action:
# actionban = https_proxy=http://proxy:3128 /usr/bin/curl ...
Fail2Ban fails to start after editing jail.local
# Check for syntax errors
sudo fail2ban-client -t
# Common issue: indentation. The second action line must be
# indented with spaces (not tabs) on the line after the first action.
# Correct:
# action = iptables-multiport[...]
# ipwhois-report[...]
#
# Wrong (no indentation):
# action = iptables-multiport[...]
# ipwhois-report[...]
Reports not appearing on bl.ipwhois.net
- Check that the IP is not a private/reserved address (10.x, 172.16-31.x, 192.168.x). The API only accepts public IPs.
- Verify curl returns a success response by running the manual test above.
- Check rate limits: the API returns
429 Too Many Requestsif you exceed 500 reports/day. Check with:curl -sI -X POST https://bl.ipwhois.net/api/report -d "ip=test"and look forX-RateLimit-Remaining. - Reports for IPs in documentation ranges (192.0.2.x, 198.51.100.x, 203.0.113.x) may be filtered.
Action runs but Fail2Ban log shows errors
# Run Fail2Ban in debug mode temporarily
sudo fail2ban-client -vvv set sshd banip 192.0.2.50
# Check the full action output
sudo journalctl -t ipwhois-bl --since "5 minutes ago"
Rate limiting reports
If your server bans hundreds of IPs per hour, you may want to rate-limit reports to stay within the API quota. Create a wrapper script:
#!/bin/bash
# Rate-limited IPWhois reporter
# Reports at most 1 IP per 10 seconds to avoid API throttling
LOCKFILE="/tmp/ipwhois-report.lock"
IP="$1"
TYPE="${2:-brute-force}"
MSG="${3:-Fail2Ban}"
# Simple lock to prevent concurrent calls from flooding
exec 200>"$LOCKFILE"
flock -w 30 200 || exit 1
/usr/bin/curl -sSf -m 10 -X POST https://bl.ipwhois.net/api/report \
-d "ip=$IP" \
-d "type=$TYPE" \
-d "message=$MSG" \
2>&1 | logger -t ipwhois-bl
sleep 2 # Brief delay between reports
exec 200>&-
sudo chmod +x /usr/local/bin/ipwhois-report.sh
Then update the action file to use the script:
actionban = /usr/local/bin/ipwhois-report.sh <ip> %(threat_type)s "Fail2Ban+<name>"
Filtering by severity (only report repeat offenders)
Use the recidive jail to only report IPs that have been banned multiple times:
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
bantime = 604800
findtime = 86400
maxretry = 3
action = iptables-allports[name=recidive]
ipwhois-report[threat_type=brute-force]
This only reports IPs that have been banned 3+ times within 24 hours, filtering out one-off attempts and reducing noise.
Custom message format with more context
Include the number of failures and the service port in the report:
[Definition]
actionban = /usr/bin/curl -sSf -m 10 -X POST https://bl.ipwhois.net/api/report \
-d "ip=<ip>" \
-d "type=%(threat_type)s" \
-d "message=Fail2Ban+jail=<name>+failures=<failures>+port=<port>+server=%(hostname)s" \
2>&1 | logger -t ipwhois-bl
actionunban =
[Init]
threat_type = brute-force
The <failures> tag is replaced by Fail2Ban with the number of failed attempts that triggered the ban. The <port> tag contains the monitored port(s).
Whitelisting IPs from being reported
To prevent reporting specific IPs (e.g., your own monitoring), use the Fail2Ban ignoreip directive:
[DEFAULT]
# These IPs will never be banned or reported
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 192.168.0.0/16 YOUR.OFFICE.IP.HERE
Here is what a complete flow looks like in your server logs when an SSH brute-force attack is detected, banned, and reported:
# 1. Attacker fails SSH login (auth.log)
Mar 15 14:22:01 web1 sshd[28401]: Failed password for root from 185.220.101.34 port 44820 ssh2
Mar 15 14:22:03 web1 sshd[28401]: Failed password for root from 185.220.101.34 port 44820 ssh2
Mar 15 14:22:05 web1 sshd[28401]: Failed password for root from 185.220.101.34 port 44820 ssh2
Mar 15 14:22:07 web1 sshd[28401]: Failed password for root from 185.220.101.34 port 44820 ssh2
Mar 15 14:22:09 web1 sshd[28401]: Failed password for root from 185.220.101.34 port 44820 ssh2
# 2. Fail2Ban detects and bans (fail2ban.log)
Mar 15 14:22:10 web1 fail2ban.actions[1205]: NOTICE [sshd] Ban 185.220.101.34
# 3. IPWhois report sent (syslog)
Mar 15 14:22:10 web1 ipwhois-bl: {"success":true,"message":"Report received"}
# 4. IP is now listed on bl.ipwhois.net
# curl https://bl.ipwhois.net/api/check?ip=185.220.101.34
# {"listed":true,"ip":"185.220.101.34","threat_type":"brute-force",
# "confidence":85,"total_reports":47,"last_seen":"2026-03-15T14:22:10Z"}