Every server exposed to the internet gets attacked. SSH brute-force bots are crawling the internet, trying thousands of username and password combinations per minute against every server with port 22 open. Check your auth log and you will see what I mean:
sudo grep "Failed password" /var/log/auth.log | tail -20
A firewall like UFW (covered in How to Setup Firewall using UFW in Ubuntu) handles the ports that should be completely closed. But it cannot help with services that must be open like SSH, your web server on port 80 and 443, any API endpoint. Those ports have to accept connections, and the bots know it.
Fail2ban fills this gap. It watches your log files in real time and automatically bans IP addresses that show signs of malicious behavior like too many failed login attempts, too many requests for suspicious URLs, any pattern you define. When an IP crosses your threshold, Fail2ban inserts a temporary firewall rule to block all further traffic from that address.
In this tutorial you will install Fail2ban on Ubuntu, learn how it works, protect SSH from brute-force attacks, add protection for Nginx, and learn how to monitor and manage bans from the command line.
How Fail2ban Works
Before touching any commands, it helps to understand what Fail2ban actually does under the hood.
Fail2ban is a daemon that runs in the background and continuously tails your log files. It applies regular expressions called filters to those logs, looking for patterns that indicate a bad actor, for example, repeated lines like:
Failed password for root from 203.0.113.45 port 54321 ssh2
When the number of matches from a single IP exceeds a threshold within a time window, Fail2ban triggers an action. The default action is to insert a rule into iptables that drops all traffic from that IP for a configurable amount of time called the ban time.
The whole setup is grouped into a jail, a named combination of:
- A log file to watch
- A filter (the regex) to apply
- Thresholds: how many failures within what time window triggers a ban
- Actions to take when the threshold is crossed
Fail2ban ships with ready-made jails for dozens of common services: SSH, Nginx, Apache, Postfix, and more. You choose which jails to enable and tune their settings.
Prerequisites
- Ubuntu 20.04, 22.04, or 24.04
- A user with
sudoprivileges - At least SSH running (Nginx is optional but covered in this guide)
- Basic familiarity with the Linux command line
Step 1: Install Fail2ban
Fail2ban is in the standard Ubuntu package repository.
sudo apt update
sudo apt install fail2ban -y
After installation, the daemon starts automatically. Verify it:
sudo systemctl status fail2ban
You should see active (running). If not, start it and enable it at boot:
sudo systemctl start fail2ban
sudo systemctl enable fail2ban
Step 2: Understand the Configuration Layout
Fail2ban keeps its configuration in /etc/fail2ban/. Two files matter most:
/etc/fail2ban/jail.confis the upstream default configuration. Do not edit this file. It gets overwritten when you update the package./etc/fail2ban/jail.localis your override file. Fail2ban reads both; settings in.localalways win over the defaults.
Filters live in /etc/fail2ban/filter.d/ and actions live in /etc/fail2ban/action.d/. The built-in ones cover most common services. You rarely need to touch them unless you are writing custom rules for your own application.
Step 3: Create Your jail.local
The jail.local file does not exist by default, so create it:
sudo nano /etc/fail2ban/jail.local
Paste this configuration as a starting point:
[DEFAULT]
# How long to ban an offending IP (in seconds). 3600 = 1 hour.
bantime = 3600
# Time window in which failures are counted (in seconds). 600 = 10 minutes.
findtime = 600
# Number of failures before a ban is triggered.
maxretry = 5
# IPs that are never banned.
ignoreip = 127.0.0.1/8 ::1
# Use iptables to enforce bans.
banaction = iptables-multiport
[sshd]
enabled = true
port = ssh
logpath = %(sshd_log)s
backend = %(sshd_backend)s
Save and close the file (Ctrl+O, Enter, Ctrl+X in nano).
Here is what each setting means:
bantime controls how long an IP stays blocked. 3600 is one hour. You can also use time suffixes: 1h, 1d, 1w. Set it to -1 for a permanent ban.
findtime is the rolling time window. If an IP accumulates maxretry failures within findtime seconds, it gets banned.
maxretry is the failure threshold. Five is a practical default, low enough to catch bots quickly, high enough that a human who forgets their password once does not get locked out.
ignoreip is a whitelist. Add your own public IP address here before enabling any jails. If you accidentally trigger a ban on yourself, you need console access to fix it. Find your current public IP with:
curl -s ifconfig.me
Add it to ignoreip:
ignoreip = 127.0.0.1/8 ::1 203.0.113.10
[sshd] enables the SSH jail. The %(sshd_log)s and %(sshd_backend)s variables resolve to the correct values for your Ubuntu version, they handle the difference between systems writing SSH logs to /var/log/auth.log versus those using the systemd journal.
Step 4: Add Nginx Protection
If you are running Nginx, add two more jails to jail.local. Open the file again:
sudo nano /etc/fail2ban/jail.local
Append these sections at the bottom:
[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 3
[nginx-botsearch]
enabled = true
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 2
nginx-http-auth bans IPs that repeatedly fail HTTP Basic Authentication challenges. Three attempts is tight but appropriate. if your protected endpoint is internal-only, you can set it even lower.
nginx-botsearch catches automated scanners that probe for common vulnerable paths like /wp-login.php, /admin, /.env, /.git/config, and similar. The built-in nginx-botsearch filter recognizes these patterns in your access log. A threshold of two is fine here because no legitimate user should be hitting these paths at all.
Make sure the log paths exist before enabling these jails:
ls /var/log/nginx/error.log /var/log/nginx/access.log
If you configured a custom log path in your Nginx server block, update logpath to match.
Step 5: Reload and Verify
After editing jail.local, tell Fail2ban to pick up the changes:
sudo systemctl reload fail2ban
Check that all your jails came up:
sudo fail2ban-client status
Expected output:
Status
|- Number of jail: 3
`- Jail list: nginx-botsearch, nginx-http-auth, sshd
Inspect a specific jail to see its filter, current failure count, and any active bans:
sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 47
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 1
|- Total banned: 5
`- Banned IP list: 203.0.113.45
The Currently failed counter tells you how many IPs are accumulating failures but have not yet crossed the maxretry threshold. Banned IP list shows the addresses currently serving a ban.
Step 6: Manually Ban and Unban IPs
You can ban an IP immediately without waiting for the threshold:
sudo fail2ban-client set sshd banip 203.0.113.45
To unban an IP:
sudo fail2ban-client set sshd unbanip 203.0.113.45
To see all currently banned addresses across all active jails:
sudo fail2ban-client banned
Common Mistakes and Troubleshooting
You locked yourself out of SSH.
If you are still connected, unban your IP immediately:
sudo fail2ban-client set sshd unbanip <your-ip>
If the session dropped, you will need console access such as VNC, KVM, or your cloud provider’s emergency console. To prevent this entirely, add your IP to ignoreip before enabling any jail.
A jail shows as inactive after reload.
Run sudo fail2ban-client status and compare the jail list to what you defined in jail.local. If a jail is missing, the most common cause is a logpath that does not exist. Fail2ban silently skips jails whose log file it cannot find. Verify using this command:
ls -la /var/log/nginx/error.log
Fail2ban is running but not banning obvious brute-force.
The filter regex may not match your log format. Test a filter against a log file directly:
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
The output shows how many lines matched. If the match count is zero, the filter does not work against your log format, possibly because your system uses a slightly different log timestamp or message format than the default regex expects.
Ban rules disappear after a reboot.
Fail2ban stores its active ban list in a database at /var/lib/fail2ban/fail2ban.sqlite3 and restores all active bans when it starts up. As long as Fail2ban is enabled to start at boot (sudo systemctl enable fail2ban), bans persist across reboots. If you find they are not, verify the service is enabled:
sudo systemctl is-enabled fail2ban
Best Practices
Put your IP in ignoreip before enabling any jail. Always. Do it first. This one habit will save you from the most painful failure mode in this entire setup.
Use incremental ban times for repeat offenders. Fail2ban supports exponential backoff through a set of bantime.* options. The first ban is short; each time the same IP is caught again, the ban time doubles. Add this to the [DEFAULT] section:
bantime.increment = true
bantime.multiplier = 2
bantime.maxtime = 604800
bantime.maxtime is the ceiling, 604800 seconds is one week. An IP that keeps attacking after short bans will eventually be blocked for a full week, which is enough to deter most persistent bots.
Match the backend to your system. On Ubuntu 20.04 and later with systemd, many services write logs to the journal rather than flat files. The %(sshd_backend)s variable handles this for the SSH jail automatically. For custom jails, set backend = systemd if the service writes to the journal, or backend = auto to let Fail2ban detect it.
Do not set maxretry below 3. A value of 1 or 2 causes false positives, a user mistyping their password once triggers a ban. Three to five is the right range for services used by real humans.
Combine Fail2ban with UFW. They complement each other perfectly. UFW enforces your static rules, closing every port you do not use. Fail2ban handles dynamic banning on the ports that must stay open. Together they cover both attack surfaces. If you have not set up UFW yet, walks through it step by step.
Disable SSH password authentication. Fail2ban dramatically reduces brute-force risk, but the best protection against SSH password attacks is to disable password authentication altogether and use SSH keys. Edit /etc/ssh/sshd_config, set PasswordAuthentication no, and restart SSH. Fail2ban then becomes a backstop rather than a primary defense.
Conclusion
You have installed Fail2ban on Ubuntu, configured jails for SSH and Nginx, and learned how to inspect and manage bans from the command line. Your server now automatically responds to brute-force attempts without any manual intervention, an IP that fails 5 SSH logins in 10 minutes gets blocked for an hour, with no action required on your part.
Fail2ban is one layer of a defense-in-depth approach, not a complete solution on its own. Pair it with:
- UFW to close ports you do not use
- SSH key authentication to make password attacks irrelevant against SSH
- Nginx rate limiting (Covered in How to Configure Rate Limiting in Nginx on Ubuntu) to protect web endpoints at the application layer before requests even hit your backend.s
With all three in place, you have covered the most common automated attack vectors that target Linux servers on the public internet.