Repository navigation
08 Fail2Ban Fundamentals
How fail2ban actually decides, bans and reports — and the configuration rules that make the difference between "it seems to work" and "I can prove it works". This page is the foundation for the next three pages.
log file / journal ──► filter (regex) ──► jail (thresholds) ──► action (ban)
│
└─► extra action: queue for AbuseIPDB
| Object | Location | Responsibility |
|---|---|---|
| filter | /etc/fail2ban/filter.d/*.conf |
one or more failregex lines; extracts <HOST>
|
| jail |
/etc/fail2ban/jail.conf, jail.d/*.conf, jail.local, jail.d/*.local
|
binds filter + log source + thresholds + actions |
| action | /etc/fail2ban/action.d/*.conf |
what to do on ban (nftables/iptables, queue, mail, ...) |
Fail2Ban reads, in this order:
-
jail.conf(shipped by the package — never edit it, upgrades overwrite it); -
jail.d/*.conf(alphabetical, package/panel files); -
jail.local(your main overrides); -
jail.d/*.local(alphabetical, last wins; the right place for small, focused files).
A later file overrides earlier keys of the same jail. This is why:
- enable and tune jails in
/etc/fail2ban/jail.localor/etc/fail2ban/jail.d/99-*.local; - the project's installer writes
/etc/fail2ban/jail.d/50-abuseipdb-reporter.local(action only) and never touches your thresholds; - a panel (ISPmanager, cPanel, ...) may regenerate
jail.local— keep your settings injail.d/99-*.localso they survive.
Validate after every edit:
fail2ban-client -t # parse the whole configuration; must print "OK"Restart vs reload.
fail2ban-client reloadreloads filters/jails, but on some versions it does not rebuild a changed action list for an existing jail. When you add/remove an action, usesystemctl restart fail2ban. Bans are restored from the database, so a restart is cheap.
[Definition]
failregex = ^<HOST> - - \[[^\]]+\] "GET /wp-login\.php
ignoreregex =Rules that save hours:
-
<HOST>is mandatory — fail2ban needs a failure-id group. Without it the filter fails withNo failure-id group. -
The date is removed before matching. Measured behaviour: fail2ban detects the timestamp,
cuts it out of the line, and applies
failregexto the remainder. Consequences:-
^<HOST> ...works for logs where the IP comes before the date (Apache/nginx combined); - a regex that expects the literal date text (
\[19/Oct/2026:...\]) will not match; - nginx error lines (
2026/10/17 14:57:26 [error] ...) become[error] ...— start the pattern with^\s*\[error\], not^\[error\].
-
- Match the minimum that identifies abuse, not the whole line. Fewer brittle groups → fewer silent breakages after log format changes.
- Test every filter against real logs (see below) — a filter that matches 0 lines is the most common outage cause.
- Use
ignoreregexfor known-good traffic (your office IP hitting a login page, monitoring).
# how many lines match, from which IPs
fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
# inspect what matched (and what did not)
fail2ban-regex /var/www/httpd-logs/*.access.log /etc/fail2ban/filter.d/nginx-botsearch.conf \
--print-all-matched | head -50
# a filter file under development
fail2ban-regex /var/log/nginx/access.log /root/my-filter.confA healthy result looks like:
Failregex: 43264 total
Lines: 96344 lines, 0 ignored, 43264 matched, 53080 missed
Zero matches on a busy log means: wrong log path, wrong format, date-stripping assumption, or a typo in the regex.
fail2ban-regexdoes not applyignoreip. It is a regex test, not a jail simulation.
| Backend | When | Notes |
|---|---|---|
systemd |
journald-only services (sshd on modern Debian/Ubuntu) | requires journalmatch, e.g. _SYSTEMD_UNIT=ssh.service + _COMM=sshd
|
file / auto
|
classic files (/var/log/auth.log, nginx, exim) |
logpath may use globs; new files are picked up on reload |
polling |
unusual filesystems/containers | slower, last resort |
For sshd prefer the journal on systemd hosts:
[sshd]
enabled = true
backend = systemd
journalmatch = _SYSTEMD_UNIT=ssh.service + _COMM=sshdFor files, verify the path actually exists and is rotated:
ls -la /var/log/auth.log /var/www/httpd-logs/*.access.logWhen logs rotate weekly, fail2ban follows the rotation — but a new virtual host log file is
only noticed after systemctl reload fail2ban (or the next restart). Remember this after adding
sites to a panel.
[DEFAULT]
findtime = 10m # window for counting failures
maxretry = 5 # failures within findtime that trigger a ban
bantime = 1d # ban duration
bantime.increment = true # repeat offenders get longer bans
bantime.factor = 2 # 1d → 2d → 4d → ...
bantime.maxtime = 12w # capTuning guidance:
| Service | findtime | maxretry | bantime | Why |
|---|---|---|---|---|
| SSH | 10m | 4 | 1d + increment | humans mistype keys rarely; bots hit constantly |
| nginx scanners | 10m | 5 | 1d | one probe is noise, five in ten minutes is a scanner |
| WordPress login (POST only) | 10m | 10 | 1d | avoid banning an admin who retries a password |
| SMTP AUTH | 1h | 15 | 1d | slow credential stuffing; fewer false positives |
| FTP | 10m | 5 | 1d | rarely legitimate repeated failures |
| Repeat offenders | 1d | 5 | 4w |
recidive jail over fail2ban.log
|
recidive is an aggregate jail: it watches fail2ban.log for repeated bans and applies a much
longer ban. Enable it and exclude it from AbuseIPDB reporting (skip_jails = recidive), otherwise
the same IP is reported twice.
[DEFAULT]
banaction = nftables
banaction_allports = nftables[type=allports]
[sshd]
port = ssh # ban only TCP/22
action = %(action_)s
%(action_abuseipdb_async)s # project action: queue a reportTwo facts that are easy to get wrong:
-
portin the jail controls the ban scope. A jail withoutportinheritsport = 0:65535fromjail.conf, which can turn a web-only jail into an all-ports block. For web jails setport = 80,443; for FTPport = 20,21,989,990. -
banaction = nftables[...]alone is not enough ifaction_passes a different port — the jail'sportwins. This was measured on a real host: a web jail banned all ports untilport = 80,443was added.
The project adds its action next to the firewall action:
[sshd]
action = %(action_)s
%(action_abuseipdb_async)saction_ is the default firewall action; the second line is the queue helper. The queue helper is
declared once in [DEFAULT] by the generated snippet:
[DEFAULT]
action_abuseipdb_async = abuseipdb-asyncnorestored = 1 inside the project action means bans restored after a fail2ban restart are not
queued again.
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.10 192.0.2.0/24Put your own addresses first, before enabling aggressive jails. For each entry ask: "if this address is banned for a day, what breaks?"
⚠️ Manual bans bypassignoreip.fail2ban-client set <jail> banip <addr>is a direct command; fail2ban does not check the ignore list. Never use your own management IP for testing. Use a documentation address (192.0.2.0/24,198.51.100.0/24,203.0.113.0/24) or a real attacker address from your logs, and be ready to unban.
Testing procedure that does not risk access:
# 1. validate config
fail2ban-client -t
# 2. see the current state
fail2ban-client status
fail2ban-client status sshd
# 3. ban a documentation IP (not your own!)
fail2ban-client set sshd banip 198.51.100.77
nft list set inet f2b-table addr-set-sshd # visible in the firewall set
fail2ban-client status sshd # counted as banned
# 4. unban immediately
fail2ban-client set sshd unbanip 198.51.100.77Use the project's dry-run instead when you only want to inspect a report payload.
| Symptom | Cause | Fix |
|---|---|---|
| filter matches nothing | wrong logpath, wrong format, date in regex |
fail2ban-regex on the real file |
No failure-id group |
<HOST> missing |
add <HOST>
|
jail does not appear in status
|
enabled = true missing / typo in section |
fail2ban-client -t, check jail.d
|
| bans all ports unexpectedly | jail has no port
|
set port = 80,443 (or the service ports) |
| action never fires | action not attached to the jail | fail2ban-client get <jail> actions |
| config disappears after panel change | edited a panel-managed file | keep overrides in jail.d/99-*.local
|
| ignore list has no effect on manual test | manual bans bypass ignoreip
|
test with an unlisted IP |
| new site logs not watched | glob resolved at start |
systemctl reload fail2ban after adding sites |
| duplicates after restart | restored bans re-run actions | action needs norestored = 1 (the project sets it) |
-
Read current config and state (
fail2ban-client status,-t, logs). -
Back up the files you will edit (
cp -a ... /root/backup-<date>/). -
Test filters with
fail2ban-regexagainst real logs (positive and negative cases). -
Validate the whole configuration with
fail2ban-client -t. -
Apply with
systemctl restart fail2ban. - Verify actions attached, jail active, a test ban works and is visible in nftables.
-
Roll back immediately if any check fails (restore backup,
-t, restart).
The project's installer follows exactly this pattern, including the fail2ban-client -t gate
before restart and a backup of every replaced file.
Next: Web Protection · SSH, Mail, FTP · Verification & Rollback
Fail2Ban_AbuseIPDB · repository · releases · issues · MIT