Introduction
Put a server on the internet and the SSH login attempts start almost immediately. The vast majority are automated bots working through common username and password lists. Two tools handle this well on Ubuntu: UFW as a simple firewall, and Fail2ban to watch the auth logs and block whoever keeps failing to log in.
By default these two don't talk to each other. Fail2ban installs its bans as raw iptables rules, while your visible firewall is UFW. That works, but your bans then live outside ufw status, which is confusing when you later try to audit what is blocked. This tutorial sets both up and, more importantly, tells Fail2ban to apply its bans through UFW, so every ban shows up as a normal UFW rule.
Prerequisites
- A server running Ubuntu 22.04 / 24.04 / 26.04 (this was tested on a fresh Ubuntu install).
- Root access, or a user with
sudo. - SSH access to the server. If you are new to UFW itself, the tutorial "Simple Firewall Management with UFW" covers the basics of rules.
Example terminology
This tutorial uses the following example values. Replace them with your own:
- IPv4 address of the server:
10.0.0.1 - Example attacker address used for the ban test:
198.51.100.23
Step 1 - Install Fail2ban
UFW already ships with Ubuntu, so there is nothing to install for the firewall itself. You can confirm it is present:
ufw versionOn a fresh Ubuntu server this returns:
ufw 0.36.2
Copyright 2008-2023 Canonical Ltd.Fail2ban is not installed by default. Update the package list and install it:
sudo apt update
sudo apt install fail2banOne thing to know about Ubuntu 22.04: after the package is installed, the service is left disabled and stopped. You can check:
systemctl is-enabled fail2ban
systemctl is-active fail2banOutput:
- Disabled
Leave it stopped for now. If you start it at this point it just loads the default jail with the default
disabled inactiveiptablesban action, which is exactly the behavior we want to change. We start it in Step 3, once the configuration is in place.
- Enabled
Stop it for now. We will restart it in Step 3, once the configuration is in place.
enabled activesudo systemctl stop fail2ban
Step 2 - Set up the UFW firewall
Before enabling the firewall, set the default policy to block incoming traffic and allow outgoing traffic:
sudo ufw default deny incoming
sudo ufw default allow outgoingNow the important part. The default policy blocks everything coming in, including your SSH session. If you enable UFW now, you will lock yourself out. Allow SSH first:
sudo ufw allow OpenSSHOpenSSH is an application profile that UFW ships with; it opens port 22/tcp. You can see the available profiles with ufw app list.
With SSH allowed, enable the firewall:
sudo ufw --force enableThe --force flag skips the interactive "this may disrupt existing ssh connections" prompt, which is what you want when you are already running over SSH and have allowed it.
Check the result:
sudo ufw status verboseStatus: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp (OpenSSH) ALLOW IN Anywhere
22/tcp (OpenSSH (v6)) ALLOW IN Anywhere (v6)The firewall is active, only SSH is open, and your session is still alive.
Step 3 - Configure Fail2ban to ban through UFW
Never edit /etc/fail2ban/jail.conf directly. It gets replaced on package updates. Fail2ban reads /etc/fail2ban/jail.local on top of it, so put your settings there.
Create /etc/fail2ban/jail.local:
sudo nano /etc/fail2ban/jail.localAdd the following:
[DEFAULT]
# Ban for 1 hour after 5 failures within 10 minutes
bantime = 1h
findtime = 10m
maxretry = 5
# Enforce bans through UFW instead of raw iptables
banaction = ufw
[sshd]
enabled = true
backend = systemdTwo lines do the real work here:
| Description | |
|---|---|
banaction = ufw |
tells Fail2ban to use the ufw action that ships in /etc/fail2ban/action.d/ufw.conf, instead of its default iptables action. Bans become UFW rules. |
backend = systemd |
For the sshd jail. On Ubuntu 22.04, OpenSSH logs to the systemd journal, so the journal backend is the reliable way for Fail2ban to read failed logins. |
Save the file. Now enable and start the service in one step:
sudo systemctl enable --now fail2banConfirm the jail is running:
sudo fail2ban-client status sshdStatus for the jail: sshd
|- Filter
| |- Currently failed: 0
| |- Total failed: 0
| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
|- Currently banned: 0
|- Total banned: 0
`- Banned IP list:Nothing is banned yet, which is expected. The Journal matches line confirms Fail2ban is reading SSH events from the journal.
Step 4 - Test that bans land in UFW
You don't want to wait for a real attacker to confirm the integration works. You can ban an address by hand and watch it appear in UFW. The address 198.51.100.23 below is a reserved documentation address, so this is safe to run.
Ban it:
sudo fail2ban-client set sshd banip 198.51.100.23The jail now lists it:
sudo fail2ban-client status sshd`- Actions
|- Currently banned: 1
|- Total banned: 1
`- Banned IP list: 198.51.100.23Now look at UFW:
sudo ufw status numbered[ 1] Anywhere REJECT IN 198.51.100.23This is the point of the whole setup. Fail2ban inserted a REJECT rule at position 1, above your allow rules, so the banned address is dropped before it ever reaches SSH. The ban is a normal UFW rule you can see and audit.
Remove the test ban:
sudo fail2ban-client set sshd unbanip 198.51.100.23Check that the rule is gone:
sudo ufw status | grep 198.51.100.23This returns nothing, which means Fail2ban removed the UFW rule when it unbanned the address.
From now on, any address that fails the SSH login five times within ten minutes gets the same treatment automatically, banned for one hour. If you watch ufw status numbered on a server that has been online for a while, you will see these rules come and go on their own.
Conclusion
You now have a UFW firewall that only allows SSH, and a Fail2ban jail that watches the SSH journal and blocks repeated failures by writing real UFW rules. Because the bans live in UFW, ufw status shows your full picture of what is blocked, instead of leaving Fail2ban's bans hidden in a separate iptables chain.
From here you can open additional ports as you add services (ufw allow 80/tcp, ufw allow 443/tcp), and add more Fail2ban jails for those services in the same jail.local file. Whenever you change jail.local, reload Fail2ban with:
sudo fail2ban-client reload