Setting Up Fail2Ban on Ubuntu: The Rules That Actually Matter for Web Servers
Every publicly accessible server gets probed continuously. Automated bots scan for open SSH ports, attempt weak credentials, and hammer web application login forms with credential stuffing lists. Without a tool actively monitoring and blocking this traffic, your server wastes resources processing requests from attackers and risks compromise through brute force or credential guessing.
Fail2Ban watches your log files and automatically blocks IP addresses that exhibit malicious behaviour. It is lightweight, configurable, and effective. This guide covers how it works, which configurations matter for web servers, what most tutorials get wrong, and how to avoid locking yourself out of your own machine.
How Fail2Ban Works
Fail2Ban operates on three concepts: filters, actions, and jails. Filters are regular expressions that match abusive patterns in log entries. Actions are the steps Fail2Ban takes when a filter matches, typically creating an iptables firewall rule to block traffic from the offending IP. Jails combine a filter with conditions specifying how many matches in what time window triggers the action.
When a jail's threshold is exceeded, Fail2Ban executes the action, which blocks the IP for a configurable ban period. When the ban expires, the firewall rule is removed. This loop runs continuously as a background service.
The default setup covers SSH out of the box. Everything else requires explicit jail configuration, including web server logins, PHP application brute force, malicious URL requests, and Apache or Nginx-specific attacks. If you are setting this up alongside other server hardening measures, a broader Ubuntu security hardening guide can help you establish a solid foundation.
Installation
Installing Fail2Ban on Ubuntu is straightforward. Update your package lists and install the package:
sudo apt update
sudo apt install fail2ban
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
sudo systemctl status fail2ban
The configuration lives in /etc/fail2ban/jail.conf. Never edit this file directly because it gets overwritten on package updates. Create your overrides in /etc/fail2ban/jail.local instead. Anything defined in jail.local takes precedence over the defaults.
The Default Section: What Each Setting Controls
The [DEFAULT] section in jail.local applies to all jails unless you override it per jail. Here is what matters:
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
destemail = [email protected]
sender = [email protected]
action = %(action_mwl)s
bantime sets how many seconds an IP stays blocked. findtime defines the monitoring window in which failures must occur. maxretry sets how many failures are allowed before a ban triggers. action_mwl bans the IP and sends an email with whois information and the log entries that triggered the ban.
The critical pair is bantime and findtime. If your ban time is shorter than the monitoring window, an attacker with enough IPs can cycle through them faster than bans expire. For SSH, a short findtime of 600 seconds with a longer bantime of 3600 seconds works well. For web application brute force where attacks may persist longer, consider a findtime of 1800 and a bantime of 7200.
You can change the action to action_ for just banning without email, or action_xarf if you have a XARF abuse reporting system configured.
SSH Jail: Real Configuration That Blocks Attacks
SSH is the most targeted service on any server with a public IP. Automated bots hammer port 22 constantly. Even if you only use key-based authentication, blocking this noise reduces log volume and frees resources. A comprehensive guide to securing SSH on Ubuntu covers additional hardening steps that work well alongside Fail2Ban.
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 3600
findtime = 600
backend = systemd
The built-in sshd filter already handles the standard SSH failed login format in auth.log. You can verify it matches correctly before going live using the fail2ban-regex tool against your own auth log:
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
Look for lines showing matched text and a nonzero hit count. If the filter returns zero matches against your auth.log when you know there have been failed logins, the regex may not match your SSH version's log format.
One setting many tutorials skip is backend = systemd. Fail2Ban defaults to auto-detecting the log backend. On systems using systemd's journal, setting the backend explicitly to systemd ensures Fail2Ban reads from the journal rather than a flat file that may not exist or may rotate in unexpected ways.
Apache and Nginx Jails
For web servers, you have several jail options depending on what you are protecting. A server hardening checklist for production Ubuntu servers provides additional context for securing web services beyond what Fail2Ban handles.
Apache HTTP Basic Auth
If you use HTTP Basic Auth for any part of your site such as admin panels, staging environments, or internal tools, enable the apache-http jail:
[apache-http-auth]
enabled = true
port = http,https
filter = apache-http-auth
logpath = /var/log/apache2/error.log
maxretry = 3
bantime = 3600
findtime = 600
Nginx HTTP Basic Auth
[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 3
bantime = 3600
findtime = 600
Nginx Bad Bots
Fail2Ban ships with a nginx-botsearch filter that matches common malicious URL patterns including admin panel access attempts, configuration file access probes, SQL injection attempts, and PHPMyAdmin scanning:
[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/access.log
maxretry = 2
bantime = 7200
findtime = 600
The nginx-botsearch filter is aggressive by default. It matches any request for paths like /wp-admin, /admin/config.php, /.env, and /phpmyadmin. If you host legitimate WordPress at /wp-admin, these requests from your own users would trigger bans. Only enable this jail if your legitimate traffic does not include these paths, or adjust the ignoreregex to exclude your own IPs.
Apache ModSecurity
If you run ModSecurity with Apache, the apache-modsecurity jail monitors the ModSecurity audit log:
[apache-modsecurity]
enabled = true
port = http,https
filter = apache-modsecurity
logpath = /var/log/apache2/modsec_audit.log
maxretry = 5
bantime = 3600
findtime = 600
Custom Filters for PHP Applications
Most PHP applications log failed login attempts in their own files rather than the web server error log. To protect a custom PHP application, you need a custom filter. First, find where the application writes authentication failure events. Common locations include /var/www/myapp/var/log/auth.log, /var/www/myapp/storage/logs/auth.log, or the syslog.
Look at the actual log format. Your filter's failregex must match the exact format of your application's log entries. Here is an example log line:
2026-05-22 14:23:11 ERROR 192.168.1.50 Failed login attempt for user: admin
The corresponding filter would be:
[Definition]
failregex = ^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} ERROR <HOST> Failed login attempt for user: admin$
ignoreregex =
Save this as /etc/fail2ban/filter.d/myapp-auth.conf. Test it before enabling:
sudo fail2ban-regex /var/www/myapp/var/log/auth.log /etc/fail2ban/filter.d/myapp-auth.conf
The output shows how many lines matched and which regex groups captured the IP. If you see zero matches but know there are failed logins, the log format does not match your regex. Adjust the pattern until it captures the real entries.
Once the test shows matches, add the jail to your jail.local:
[myapp-auth]
enabled = true
port = http,https
filter = myapp-auth
logpath = /var/www/myapp/var/log/auth.log
maxretry = 5
bantime = 3600
findtime = 600
WordPress and Common CMS
WordPress logs failed login attempts to /wp-login.php in the Apache access log with a 200 status code and a query string containing log and pwd. A simple filter for WordPress admin protection requires both a jail entry and a custom filter file.
Jail configuration in jail.local:
[wordpress]
enabled = true
port = http,https
filter = wordpress
logpath = /var/log/apache2/access.log
maxretry = 5
bantime = 3600
findtime = 600
Filter file at /etc/fail2ban/filter.d/wordpress.conf:
[Definition]
failregex = ^<HOST> - .* "POST /wp-login.php.*" 200
ignoreregex =
Many WordPress sites also have the XML-RPC interface enabled, which is frequently abused for brute force and pingback attacks. To monitor and block XML-RPC abuse, add this jail:
[wordpress-xmlrpc]
enabled = true
port = http,https
filter = wordpress-xmlrpc
logpath = /var/log/apache2/access.log
maxretry = 3
bantime = 7200
findtime = 600
Filter file at /etc/fail2ban/filter.d/wordpress-xmlrpc.conf:
[Definition]
failregex = ^<HOST> - .* "POST /xmlrpc.php.*"
ignoreregex =
Recursive Fail2Ban: Ban Hosts That Scan
A useful configuration for high-traffic servers is monitoring for recursive scan patterns. This means an IP hitting many different non-existent URLs in rapid succession, which often indicates a vulnerability scanner or a compromised host being used for reconnaissance.
[apache-noscript]
enabled = true
port = http,https
filter = apache-noscript
logpath = /var/log/apache2/access.log
maxretry = 10
bantime = 7200
findtime = 600
This jail blocks IPs that trigger a high number of 404 errors for missing scripts including .php, .pl, and .cgi files. The threshold of 10 is adjustable. Lower it on low-traffic sites and raise it on sites with many legitimate varied URLs. The Fail2Ban setup guide covers additional jail configurations for common attack patterns.
Whitelist Correctly
The ignoreip setting prevents bans on IPs you control. Add it to the [DEFAULT] section:
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.50 10.0.0.0/8
Specify individual IPs or CIDR ranges as needed. Do not whitelist large ranges without reason. Whitelisting 10.0.0.0/8 exempts millions of IPs from all bans. Only whitelist ranges you control or trust absolutely.
If you manage the server via a specific VPN or jump host, add that IP explicitly. If your office uses a fixed IP, add it. If you rely on dynamic DNS for remote access, you cannot safely whitelist it. Instead, set a higher maxretry for your services or implement certificate-based access controls.
Persistent Bans with SQLite
Fail2Ban stores active bans in memory by default. If the service restarts, all ban state is lost and repeat offenders can reconnect immediately. To persist bans across restarts, enable the database backend in your jail.local:
[DEFAULT]
dbfile = /var/lib/fail2ban/fail2ban.sqlite3
With the database backend, you can also implement progressively longer bans for repeat offenders using the recidive jail. This jail bans IPs that have been banned repeatedly within a longer time window:
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
action = iptables-allports
maxretry = 2
bantime = 604800
findtime = 86400
The recidive jail triggers a seven-day block for IPs that trigger bans repeatedly. An IP that gets banned three times in one day clearly is not giving up and deserves a longer block.
Monitoring: Seeing What Fail2Ban Does
Fail2Ban ships with a client tool for inspecting active state:
sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client get sshd bantime
sudo fail2ban-client get sshd findtime
The first command lists all jails and their ban counts. The second shows details for the SSH jail specifically. The remaining commands retrieve current settings for the SSH jail.
Active bans are stored in the iptables filter table. You can also see them with raw iptables:
sudo iptables -L f2b-sshd -n --line-numbers
Log output goes to /var/log/fail2ban.log. Review it periodically to understand what is being blocked. If you see unusual patterns such as bans from IPs in a specific geographic range or attacks targeting a specific application endpoint you did not know was exposed, adjust your jail configurations accordingly.
Set up a monitoring check that alerts you if the fail2ban service stops. A simple systemd timer or Monit check that runs sudo fail2ban-client ping and alerts on failure is sufficient. If Fail2Ban is not running, your server is unprotected.
Common Mistakes and What to Do Instead
Setting maxretry = 1 on a shared IP catches many people out. If your office has five people behind a NAT gateway, one person triggering a false positive locks out all five. Set maxretry high enough that a single user's accidental mistypes do not cause collateral damage.
Enabling the nginx-botsearch jail without adjusting for your actual traffic catches another common mistake. If your site legitimately serves /admin or /wp-admin to users, enabling that jail as-is will ban your own users. Either exclude your office IP range from ignoreip, or adjust the jail's findtime and maxretry so that normal use does not trigger it.
Disabling fail2ban because it caused operational friction is the wrong response. The solution to accidental self-locks is not to disable the protection. It is to tune the thresholds and add your IP to ignoreip.
Configuring ban times that are too short to matter catches beginners. A one-minute ban does not stop a determined attacker because they reconnect as soon as the block lifts. For anything other than testing, ban times of at least an hour are more effective.
What Fail2Ban Does Not Cover
Fail2Ban is a server-local tool. It can only act on what it sees in your logs. It cannot block volumetric DDoS attacks that would overwhelm your server before log analysis can keep up. It cannot protect against attacks that come from a rotating pool of thousands of IP addresses faster than you can ban them.
For those scenarios, a cloud WAF or network-layer DDoS mitigation service is required. Fail2Ban handles the day-to-day abuse including SSH brute force, application login brute force, vulnerability scanning from individual IPs, and coordinated manual attacks from a limited number of sources. It is the right tool for the threats that hit most servers continuously, and it does that job well.
For web applications under active attack, also consider application-level controls. CAPTCHA on login forms after failed attempts, rate limiting at the application layer, and multi-factor authentication all raise the bar for attackers. Layering these controls with Fail2Ban creates a more robust defence.
Getting the Configuration Right
Fail2Ban works best when it is tuned to your specific environment. The default configuration is a starting point, not a finished setup. Review your actual traffic patterns, identify which services are exposed, and configure jails that match your legitimate usage.
Test your filters before enabling jails in production. Use fail2ban-regex against real log data to verify that your patterns match what you expect. A filter that returns zero matches against real attack data provides no protection regardless of how well it looks on paper.
Monitor your Fail2Ban installation over time. Check the logs, review ban patterns, and adjust thresholds as your traffic evolves. A configuration that works well today may need tuning as your application grows or as new attack patterns emerge.