Repository navigation
03 Architecture
Two paths that never block each other: the ban path is local and instant; the report path is scheduled and tolerant.
flowchart LR
A[fail2ban jail<br/>any filter] -->|actionban| B[nftables ban<br/>instant]
A -->|actionban| C[abuseipdb-async<br/>enqueue]
C --> D[(SQLite queue<br/>events + reports)]
E[systemd timer / cron<br/>16 min] --> F[abuseipdb-report worker]
D --> F
F -->|read evidence| G[(fail2ban.sqlite3<br/>bips.data.matches)]
F -->|POST /api/v2/report| H[AbuseIPDB API]
F -->|retry / journal| D
I[/etc/abuseipdb/.abuseipdb.key<br/>mode 600/] --> F
J[updater.json<br/>GitHub] --> K[abuseipdb-upgrade] -->|install.sh| A
| Component | Responsibility |
|---|---|
fail2ban/action.d/abuseipdb-async.conf |
action norestored = 1; calls the queue helper with <ip>, <name>, <bantime>, <failures> only |
bin/abuseipdb-enqueue |
validates the address, applies ignore rules, writes one event (~0.1 s) |
src/abuseipdb_reporter/db.py |
SQLite schema, dedupe, state transitions, retention |
src/abuseipdb_reporter/evidence.py |
reads bips.data.matches from fail2ban's DB; classifies lines |
src/abuseipdb_reporter/comments.py |
builds the PII-free comment, categories, timestamp, attribution |
src/abuseipdb_reporter/api.py |
urllib client (no dependencies) |
src/abuseipdb_reporter/reporter.py |
run loop, budgets, throttles, error handling |
src/abuseipdb_reporter/keys.py |
key resolution, key file, first-run prompt, migration |
src/abuseipdb_reporter/updater.py |
manifest check, SHA-256 verify, safe extraction, installer call |
bin/abuseipdb-report |
worker CLI (--stats, --dry-run, --check-key, --setup, --key-status) |
bin/abuseipdb-upgrade |
self-update CLI (--check, --upgrade, --json, --dry-run) |
- Fail2Ban matches a filter and creates a ban.
- The ordinary firewall action applies the ban (nftables by default in the Debian packaging).
- The extra
abuseipdb-asyncaction runsabuseipdb-enqueue, which:- parses the IP with
ipaddress; - refuses private/loopback/link-local/documentation/reserved ranges (unless explicitly overridden for tests);
- refuses jails listed in
skip_jails(defaultrecidive); - inserts (or updates, within a 5-minute dedupe window) one row in
events.
- parses the IP with
No network access, no secrets, no attacker-controlled text on a command line.
Every 16 minutes (or on manual systemctl start abuseipdb-report):
-
/runlock — one worker at a time. - Select pending IPs, oldest first.
- Skip an IP if its last successful report is younger than
min_interval_ip(default 1200 s; AbuseIPDB itself enforces 900 s). - Stop when the daily budget (
max_reports_per_day, 900) or per-run budget (max_reports_per_run, 25) is exhausted. - Read evidence for each jail from fail2ban's database.
- Build the comment, categories and timestamp.
-
POST /api/v2/report. - Interpret the response (table below), journal the attempt, commit.
- Delete finished events and old report rows older than
retention_days.
events(id, ip, jail, event_time, bantime, failures, created_at,
reported, attempts, note)
reports(id, ip, jail, reported_at, status, categories, comment, response)events.reported: 0 pending · 1 reported · 2 invalid report · 3 gave up.
Dedupe: repeated bans of the same IP+jail while the previous event is still pending (5-minute window) update the row instead of creating dozens of events. A persistent attacker therefore produces one report per throttle window.
| Operation | Result |
|---|---|
| enqueue (one event, SQLite WAL) | ≈0.1 s (10 calls in 1.06 s, Python start-up included) |
| worker, empty queue | well under 1 s; ~13 MB RSS |
| lock fallback |
/run → /var/lock → $TMPDIR; if no path is writable the worker runs without the lock (so CI/unprivileged dry-runs do not silently skip) |
The comment is built from the log lines that fail2ban already matched. They live in fail2ban's own SQLite database:
SELECT data FROM bips WHERE ip=? AND jail=?;
-- data: {"matches": ["2026-... sshd[...]: Failed password for root ...", ...], "failures": 4}Properties:
- exactly what triggered the ban — no re-parsing of 100 MB of web logs per run;
- no shell involved: text goes into a Python string, then into a form-encoded API call;
- the reporter strips timestamps and syslog hostnames for readability;
- for SSH/FTP/mail jails only generic patterns are published (
root password attempts,invalid usernames tried), never the attacker-chosen usernames; - for web jails the attacker's own URLs are published (
GET /.env -> 404) — useful and safe; - if fail2ban has no matches (e.g. a manual ban), the comment simply omits the evidence block.
| Status | Meaning | Action |
|---|---|---|
| 200 | accepted | mark events reported, journal the attempt |
| 401 | bad API key | stop the run, log an error; next tick retries after you fix the key |
| 429 “once in 15 minutes” | per-IP limit | leave pending, retry on a later tick |
| 429 other | daily/global limit | stop the run, keep everything pending |
| 400 / 422 | invalid report | mark invalid, never retry |
| 5xx / timeout | transient | increment attempts, retry later; after 10 attempts mark “gave up” |
- The key is resolved at runtime by
keys.py(CLI → env → key file → legacy config → prompt). - It is stored in
/etc/abuseipdb/.abuseipdb.key, never inreporter.conf, never in the tarball,.gitignoreexcludes*.key. -
updater.jsonis the release manifest.updater.pyrequires a SHA-256 checksum, refuses archives without one, rejects path traversal/symlinks/devices, snapshots the key file before runninginstall.shand restores it if the install fails.
| Failure | Behaviour |
|---|---|
| AbuseIPDB down | events stay queued; retried on later ticks |
| API key revoked | 401, run stops, queue intact, clear error |
| daily quota exhausted | run stops until the next day (local midnight of the host) |
| SQLite locked | WAL + 10 s busy timeout |
| disk full | enqueue logs an error; the ban still happens |
| reboot | queue on disk; Persistent=true catches up the missed tick |
| fail2ban restart |
norestored = 1 — restored bans are not re-queued |
| worker crash | unfinished events stay pending; next tick retries |
| updater download corrupted | checksum mismatch aborts before extraction |
No daemon, no web UI, no score-based blocking, no auto-update in the background, no storage of request bodies or mail content.
Fail2Ban_AbuseIPDB · repository · releases · issues · MIT