How to Install and Configure Fail2ban on Ubuntu and Debian

A clipboard showing log entries where the final repeated line is heavily struck out by a rubber stamp. Text: INSTALL FAIL2BAN

We may earn a referral fee at no additional cost to you when you shop through the links on this page.

Put a server on a public IP and automated scanners will find the SSH port and start trying passwords. Fail2ban reads the log, spots the pattern, and drops the offender into a firewall rule. On Ubuntu and Debian it starts protecting SSH the moment the package lands, before you configure anything.

Before You Start 

Fail2ban edits firewall rules and reads system logs, so it needs a machine you actually administer:

  • A server running Ubuntu 22.04 or later, or Debian 13
  • A user account with sudo privileges
  • SSH access, and ideally a second way in (a provider console) in case a jail bites you

This is not generally something you can add to shared hosting. Installing a package and rewriting firewall rules both need root, so on a shared plan you are relying on whatever protection the host runs for you.

Install Fail2ban 

Fail2ban is in the default repositories on every supported Ubuntu and Debian release, so there is no PPA to add:

Terminal
sudo apt update
sudo apt install fail2ban

The Debian packaging enables the SSH jail for you. It ships a file at /etc/fail2ban/jail.d/defaults-debian.conf that turns the sshd jail on, which is why brute-force protection is live before you have written any configuration of your own:

Terminal
cat /etc/fail2ban/jail.d/defaults-debian.conf

That file is package-owned, so treat it as read-only. Everything you change goes somewhere else, which the configuration section below covers.

How Fail2ban Decides to Ban 

A jail ties three things together: a source of events (a log file or the systemd journal), a filter holding the regular expressions that recognise a failure, and an action that blocks the address. The decision itself comes down to three numbers:

SettingMeans
maxretryHow many failures a host is allowed before it is banned
findtimeThe window those failures have to happen within
bantimeHow long the ban lasts

A host is banned once it reaches maxretry failures inside the last findtime seconds, and the block stays in place for bantime. The comparison is at least, not more than, so with maxretry = 5 the fifth failure is the one that bans, not the sixth. Each of these accepts either a plain number of seconds or an abbreviation, so 600 and 10m are the same value written two ways.

Worked through with the shipped defaults, that means five failed logins inside a ten-minute window earn a ten-minute ban. An attacker making four attempts an hour never trips it, and neither does someone who fumbles a passphrase twice.

Ten minutes is deliberately short. It is enough to break the rhythm of an automated scanner without stranding a legitimate user for the afternoon, and it is the first value most people raise once they have watched their logs for a week.

The counter is per IP address, not per person. If your office, VPN or CGNAT provider puts many people behind one public address, a single misconfigured script can get everyone on that address banned together. Put the addresses you trust in ignoreip before you tighten anything.

Confirm It Is Actually Running 

Two commands, and they answer different questions. The first asks whether the service came up, the second asks whether it is actually watching anything:

Terminal
sudo systemctl status fail2ban
sudo fail2ban-client status

A healthy install lists the jails it has loaded:

Output
Status
|- Number of jail:	1
`- Jail list:	sshd

Then look at the jail itself. This is the command worth remembering, because it tells you what Fail2ban is reading, how many failures it has counted, and who is currently blocked:

Terminal
sudo fail2ban-client status sshd
Output
Status for the jail: sshd
|- Filter
|  |- Currently failed:	0
|  |- Total failed:	0
|  `- File list:	/var/log/auth.log
`- Actions
   |- Currently banned:	0
   |- Total banned:	0
   `- Banned IP list:

The File list line is the one to read carefully. On some releases it names a log file, on others the jail reads the systemd journal instead and this line looks different. That difference is the subject of a later section, and it is the most common reason a working-looking install bans nobody.

Fail2ban also keeps its own log, which is where bans and unbans are recorded as they happen. On Ubuntu and Debian that is /var/log/fail2ban.log:

That path is not guaranteed. Fail2ban writes wherever its logtarget setting points, and on some minimal cloud images the package is configured to log to the journal instead, so the file never appears. Both readings are below; use whichever your box has:

Terminal
sudo tail -n 50 /var/log/fail2ban.log
sudo journalctl -u fail2ban -n 50
sudo grep -E 'Ban|Unban' /var/log/fail2ban.log
sudo journalctl -u fail2ban | grep -E 'Ban|Unban'

If a jail is running but never bans anything, the usual cause is that the filter’s patterns do not match the log lines your system actually writes. fail2ban-regex settles that question directly: give it a log source and a filter, and it reports how many lines matched.

Feed it the source your jail is actually reading, which is the File list or journal line you just checked. Use the first form for a file-backed jail and the second for a journal-backed one:

Terminal
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
sudo fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf

The number to read is the last line of the report:

Output
Lines: 1 lines, 0 ignored, 1 matched, 0 missed

matched greater than zero proves the filter recognises the lines in the sample you gave it, which is the usual thing standing between you and a working jail, though it does not by itself prove the jail is banning: the jail still has to be enabled, reading that same source, and seeing enough failures inside findtime. Everything landing in missed means the filter is not recognising your log format, and no amount of tuning maxretry will help until that is fixed.

Configure It in jail.local, Never jail.conf 

Fail2ban ships its defaults in .conf files and expects you to leave them alone so package upgrades stay clean. Your settings go in a matching .local file, which is parsed afterwards and overrides what the .conf file set. jail.conf says so itself, in a comment at the top telling you not to modify it.

The useful consequence is that a .local file holds only the keys you are actually changing. There is no need to copy jail.conf and edit it. Create the file:

Terminal
sudo nano /etc/fail2ban/jail.local

A reasonable starting point, which lengthens the ban and whitelists your own address so you cannot lock yourself out:

/etc/fail2ban/jail.local
[DEFAULT]
bantime  = 1h
findtime = 10m
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 203.0.113.42

[sshd]
enabled = true

Save and exit (press CTRL + X, press Y, then press ENTER)

Replace 203.0.113.42 with the address you administer the server from. Then reload so the changes take effect:

Terminal
sudo systemctl restart fail2ban
sudo fail2ban-client status sshd

For the record, the full parse order is jail.conf, then jail.d/*.conf alphabetically, then jail.local, then jail.d/*.local, and the last file to set a value wins. That is why your jail.local beats the packaged defaults-debian.conf.

What Changed Between Ubuntu and Debian Releases 

This is where copied-from-a-tutorial advice quietly stops working. The Fail2ban defaults on Ubuntu and Debian have changed twice in recent releases, and the changes are not cosmetic: they alter which firewall does the banning and where the jail looks for failures.

ReleaseFail2banDefault banactionsshd event source
Ubuntu 22.040.11.2iptableslog file
Ubuntu 24.041.0.2nftablessystemd journal
Ubuntu 26.041.1.0nftablessystemd journal, per jail
Debian 131.1.0nftablessystemd journal, per jail

Two things follow. First, the widely repeated advice that Fail2ban bans through iptables and that its bans are therefore invisible to ufw status is now out of date for Ubuntu 24.04 and later, which ban through nftables instead. The bans are still invisible to ufw status, but for a different reason, and the iptables -L command those tutorials tell you to run will show you nothing.

Second, a jail reading the systemd journal does not use logpath at all. It uses a journalmatch expression from the filter to select journal entries. Setting logpath on a journal-backed jail has no effect, which is exactly the kind of edit that leaves someone convinced Fail2ban is broken.

If you are unsure which of these your own box is doing, ask Fail2ban rather than guessing from the release number. The -d flag dumps the configuration it actually resolved, after every .conf, .d and .local file has been merged:

Terminal
sudo fail2ban-client -d | grep -E 'sshd.*(backend|addlogpath|addjournalmatch|addaction)'

That is the authoritative answer for your machine, and it is worth running once after any jail.local edit, because it shows the merged result rather than what you think you wrote.

If you want bans to show up in ufw status alongside your other rules, Fail2ban ships an action for it at /etc/fail2ban/action.d/ufw.conf. Set banaction = ufw under [DEFAULT] in jail.local. Two catches. UFW is not installed on every image, so run command -v ufw first and sudo apt install ufw if it is missing. And the action file’s own header notes that you must have run ufw enable before it does anything. Allow SSH before you enable it. UFW’s default policy denies incoming traffic, so on a machine you reach over SSH, sudo ufw enable on its own ends the session you are typing in and does not give it back. Run sudo ufw allow OpenSSH first, or sudo ufw allow 22/tcp, and confirm it appears in sudo ufw status before enabling.

When Fail2ban Bans You 

It will happen. You mistype a passphrase a few times, or a deployment script retries, and the jail you just configured locks out the address you administer from. First, find out whether that is really what happened:

Terminal
sudo fail2ban-client status sshd

The Banned IP list line at the bottom names every address currently blocked. Remove yours:

Terminal
sudo fail2ban-client set sshd unbanip 203.0.113.42

The command prints the number of addresses it unbanned, so 1 means it worked and 0 means that address was not banned in the first place. The lasting fix is the ignoreip line from earlier: it accepts individual addresses, CIDR ranges and hostnames, and hosts matching it are never banned.

None of that helps if you are already locked out of SSH, which is the one failure mode worth planning for before you start rather than after. You need a way in that does not depend on port 22. Check now, while you still have a working session, whether your provider gives you an out-of-band console, and how you would reach it. Finding that out during a lockout is considerably less pleasant.

Protecting More Than SSH 

Fail2ban ships filters for plenty of services besides SSH, including Apache, Nginx, Postfix and ProFTPD. They are defined but switched off; you enable them in jail.local the same way. The Nginx basic-auth jail is a common second one:

Terminal
sudo nano /etc/fail2ban/jail.local
/etc/fail2ban/jail.local
[nginx-http-auth]
enabled  = true
port     = http,https
backend  = auto
logpath  = /var/log/nginx/error.log

Save and exit (press CTRL + X, press Y, then press ENTER)

Editing the file changes nothing on its own, but do not reload yet. Confirm the log file you named actually exists first, because of what the callout below describes: get it wrong and the reload takes the whole service down, SSH jail included.

Terminal
ls -l /var/log/nginx/error.log

Once that file is there, reload and check the jail is genuinely loaded rather than assuming:

Terminal
sudo systemctl reload fail2ban
sudo systemctl status fail2ban
sudo fail2ban-client status

The backend and logpath lines are there on purpose, and they do different jobs. If your global [DEFAULT] sets backend = systemd, as Ubuntu 24.04 does, a web-server jail inherits it and goes looking in the journal for entries Nginx wrote to a file. backend = auto is the line that overrides that and puts the jail back on file reading; logpath then tells it which file. On its own logpath would do nothing, which is the same trap described earlier from the other direction.

Before enabling a web-server jail, check that the log it expects is the log your server actually writes, because getting it wrong is far more expensive than it sounds. Our guide to where Apache keeps its error and access logs covers the paths per distribution if you are enabling the Apache jails.

A file-backed jail whose logpath does not exist does not fail quietly, and it does not fail alone. Tested on Ubuntu 22.04, 24.04, 26.04 and Debian 13, adding one jail with a missing log file stopped the whole Fail2ban service from starting: systemctl is-active fail2ban returned failed, no jail loaded, and fail2ban-client could not even reach the socket. Your working SSH jail goes down with it, and nothing tells you except the service status. Always run sudo systemctl status fail2ban after adding a jail.

Where to Go From Here 

Fail2ban is mitigation, not a fix. It raises the cost of guessing a password; it does not stop anyone who already has one. The change that actually removes password guessing as a threat is turning passwords off entirely, so if you have not already moved to SSH key authentication, do that first and treat Fail2ban as the layer that keeps the noise out of your logs.

Support DevAnswers
Written, tested and maintained by one developer. Your support keeps these guides accurate and ad-light.
Buy me a coffee

Comments

Leave a reply