Fail2ban custom jails let you ban an IP based on lines in your own application log, not just SSH or Nginx. You write a filter containing a regex that captures the attacker's address as <HOST>, test it with fail2ban-regex, then enable a jail in jail.local that watches that file. This guide builds one complete example end to end.
The examples work on Ubuntu 22.04/24.04, Debian 12, AlmaLinux 8/9 and Rocky 8/9. Adjust paths and service names for your stack.
Before you start
You need:
- Root or
sudoaccess to the server. - Fail2ban already installed and running. If not, follow protect SSH from brute-force attacks with Fail2ban first.
- One application log file that records the client IP on every failure. A line must contain the IP in plain text; if your app logs the reverse proxy address instead, fix that first (see the troubleshooting section).
- Basic comfort with regular expressions and the
fail2ban-regextool.
Warning: Editing Fail2ban configuration can stop the service from starting and leave you with no brute-force protection at all. Always validate with fail2ban-client -t before reloading, and keep an existing SSH session open while you work.
The example application writes to /var/log/myapp/auth.log with lines like this:
2026-01-01 10:12:33 WARN login failed for user deploy from 203.0.113.10 (bad password)Understand how a custom jail reads a log
Two files are involved and they are not the same thing:
- A filter in
/etc/fail2ban/filter.d/holds the[Definition]block withfailregex. It describes what a bad line looks like. No paths, no ban times. - A jail in
/etc/fail2ban/jail.localreferences that filter by name and adds the log path, retry limits, ban duration and action.
Never edit jail.conf or the shipped filters in filter.d/; package updates overwrite them. Use jail.local and filter.d/<name>.conf (or a .local override) instead.
Fail2ban reads new lines appended to the log. At startup it seeks to the end of the file, so only lines written after the jail is active are counted.
Find the exact log line to match
Look at real failures before writing any regex:
tail -n 50 /var/log/myapp/auth.log
grep -c 'login failed' /var/log/myapp/auth.logCopy one line verbatim. Note the timestamp format, the field separator and whether the IP is always in the same position. A regex that matches only one variant of the message will miss everything else, so check a few different failure types with grep -E.
Write the filter regex
Create /etc/fail2ban/filter.d/myapp-auth.conf:
[Definition]
# 2026-01-01 10:12:33 WARN login failed for user deploy from 203.0.113.10 (bad password)
failregex = ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} WARN\s+login failed for user \S+ from <HOST>
ignoreregex =
datepattern = ^%%Y-%%m-%%d %%H:%%M:%%SKey points:
<HOST>is the placeholder Fail2ban replaces with an IP (or hostname) pattern. Without it, the filter matches but nothing can be banned.\s+handles variable whitespace between columns; a literal space often fails.%%indatepatternis an escaped percent sign. Fail2ban expands it to%before handing the format to Python'sstrptime.- Set
datepatternexplicitly whenever your log does not use the default syslog timestamp. A wrong date pattern is the most common reason a filter reports zero matches.
To ban only the username pattern and log it, add a named group:
failregex = ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} WARN\s+login failed for user (?P<user>\S+) from <HOST>Test the filter with fail2ban-regex
Never enable an untested filter. Run it against the live log or a copy:
fail2ban-regex /var/log/myapp/auth.log /etc/fail2ban/filter.d/myapp-auth.confYou want output similar to:
Lines: 1840 lines, 0 ignored, 37 matched, 1803 missed
[1] ^\d{4}-\d{2}-\d{2} ... from <HOST>
37/37 hitsCheck the Date template hits section too. If it shows 0 hits, your datepattern does not match the log and the jail will never ban anyone. To see what failed, run:
fail2ban-regex -v --print-all-missed /var/log/myapp/auth.log /etc/fail2ban/filter.d/myapp-auth.conf | lessTip: You can test an inline regex before creating a file: fail2ban-regex /var/log/myapp/auth.log '^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} WARN\s+login failed for user \S+ from <HOST>'.
Warning: A poorly written regex with nested quantifiers can take minutes per line on a large log. Test against a trimmed copy (head -n 20000 /var/log/myapp/auth.log > /tmp/test.log) so you do not stall the server.
Create the custom Fail2ban jail
Add the jail to /etc/fail2ban/jail.local. Create the file if it does not exist, and put the [DEFAULT] section at the top:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.10
[myapp-auth]
enabled = true
filter = myapp-auth
logpath = /var/log/myapp/auth.log
port = http,https
backend = auto
maxretry = 5
findtime = 10m
bantime = 1hNotes on each line:
filtermust match the filename infilter.d/without the.confsuffix.portdecides which ports the ban action blocks. If you omit it, the jail inherits the[DEFAULT]port, which is usuallysshand therefore useless for a web application.ignoreipshould always contain your own admin addresses, or you can lock yourself out. See configure jail.local settings for howbantime,findtimeandmaxretryinteract.- Use a glob such as
logpath = /var/log/myapp/*.logif the app rotates into dated files.
If the application logs only to the systemd journal, replace logpath with:
backend = systemd
journalmatch = _SYSTEMD_UNIT=myapp.serviceIf the app sits behind Nginx or another proxy, it will log the proxy IP. Configure it to trust and record X-Forwarded-For first, otherwise you will ban your own load balancer.
Reload Fail2ban and confirm the jail is running
Validate the configuration, then reload:
fail2ban-client -t
systemctl reload fail2ban
systemctl status fail2ban --no-pagerConfirm the jail exists and is watching the file:
fail2ban-client status
fail2ban-client status myapp-authThe second command prints the filter list, the log file path and the current banned IPs. The Total line count should be close to the number of lines in the file and should grow over time.
Check it works
Append synthetic failures and watch the ban appear. Run this five times, once per maxretry:
printf '%s WARN login failed for user deploy from 203.0.113.10 (bad password)\n' \
"$(date '+%Y-%m-%d %H:%M:%S')" >> /var/log/myapp/auth.logThen check:
fail2ban-client status myapp-authYou should see 203.0.113.10 in the Banned IP list. Confirm the firewall rule exists:
iptables -S | grep f2b
# or, on nftables systems:
nft list ruleset | grep -A5 f2bClean up the test ban and the fake lines:
fail2ban-client set myapp-auth unbanip 203.0.113.10If you use a firewall front end such as firewalld, the ban is applied through a different action — see Fail2ban with firewalld or nftables.
Troubleshooting
- Jail shows
Total line count: 0or "No file found" — thelogpathis wrong, the file does not exist yet, or the glob matches nothing. Check withls -l /var/log/myapp/and correct the path. Fail2ban only discovers new files when the jail is reloaded. fail2ban-regexmatches lines but the jail never bans — the timestamps are not parsed. Look at theDate template hitsblock and setdatepatternto match your log exactly.- Lines match, retries are reached, no ban — the IP is in
ignoreip, or the app logs the reverse proxy address instead of the real client. Fix the application's trusted-proxy setting so it writes the original IP. - Ban appears but traffic still gets through — the jail's
portdoes not include the service port, or the ban action targets a different firewall backend than the one in use. Checkiptables -Sversusnft list ruleset. - Fail2ban will not start after your edit — a regex or config syntax error. Run
fail2ban-client -t, then read/var/log/fail2ban.logfor the exact line number.
For persistent problems, see Fail2ban not banning? Common causes and fixes and check banned IPs and unban an address.
Next steps
- Write a custom Fail2ban filter and test it with fail2ban-regex for more filter patterns and edge cases.
- Configure Fail2ban jails: bantime, findtime and maxretry to tune how aggressively the jail reacts.
- Protect Nginx with Fail2ban: bad bots, scanners and auth if the same application is served through Nginx.
- Get email or Telegram alerts when Fail2ban bans an IP so you notice attacks without watching the log.
