Skip to content

08 Fail2Ban Fundamentals

Mikhail Deynekin edited this page Oct 20, 2026 · 1 revision

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.


Mental model

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, ...)

Configuration precedence (the rule people break)

Fail2Ban reads, in this order:

  1. jail.conf (shipped by the package — never edit it, upgrades overwrite it);
  2. jail.d/*.conf (alphabetical, package/panel files);
  3. jail.local (your main overrides);
  4. 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.local or /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 in jail.d/99-*.local so they survive.

Validate after every edit:

fail2ban-client -t          # parse the whole configuration; must print "OK"

Restart vs reload. fail2ban-client reload reloads filters/jails, but on some versions it does not rebuild a changed action list for an existing jail. When you add/remove an action, use systemctl restart fail2ban. Bans are restored from the database, so a restart is cheap.


Filters: failregex essentials

[Definition]
failregex = ^<HOST> - - \[[^\]]+\] "GET /wp-login\.php
ignoreregex =

Rules that save hours:

  1. <HOST> is mandatory — fail2ban needs a failure-id group. Without it the filter fails with No failure-id group.
  2. The date is removed before matching. Measured behaviour: fail2ban detects the timestamp, cuts it out of the line, and applies failregex to 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\].
  3. Match the minimum that identifies abuse, not the whole line. Fewer brittle groups → fewer silent breakages after log format changes.
  4. Test every filter against real logs (see below) — a filter that matches 0 lines is the most common outage cause.
  5. Use ignoreregex for known-good traffic (your office IP hitting a login page, monitoring).

Testing filters with real data

# 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.conf

A 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-regex does not apply ignoreip. It is a regex test, not a jail simulation.


Backends and log sources

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=sshd

For files, verify the path actually exists and is rotated:

ls -la /var/log/auth.log /var/www/httpd-logs/*.access.log

When 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.


Thresholds: findtime, maxretry, bantime

[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    # cap

Tuning 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.


Actions and port scope (a measured trap)

[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 report

Two facts that are easy to get wrong:

  1. port in the jail controls the ban scope. A jail without port inherits port = 0:65535 from jail.conf, which can turn a web-only jail into an all-ports block. For web jails set port = 80,443; for FTP port = 20,21,989,990.
  2. banaction = nftables[...] alone is not enough if action_ passes a different port — the jail's port wins. This was measured on a real host: a web jail banned all ports until port = 80,443 was added.

The project adds its action next to the firewall action:

[sshd]
action = %(action_)s
         %(action_abuseipdb_async)s

action_ 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-async

norestored = 1 inside the project action means bans restored after a fail2ban restart are not queued again.


Whitelists and safe testing

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.10 192.0.2.0/24

Put your own addresses first, before enabling aggressive jails. For each entry ask: "if this address is banned for a day, what breaks?"

⚠️ Manual bans bypass ignoreip. 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.77

Use the project's dry-run instead when you only want to inspect a report payload.


Common configuration mistakes

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)

Safe change workflow (use this every time)

  1. Read current config and state (fail2ban-client status, -t, logs).
  2. Back up the files you will edit (cp -a ... /root/backup-<date>/).
  3. Test filters with fail2ban-regex against real logs (positive and negative cases).
  4. Validate the whole configuration with fail2ban-client -t.
  5. Apply with systemctl restart fail2ban.
  6. Verify actions attached, jail active, a test ban works and is visible in nftables.
  7. 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

Clone this wiki locally