# Do You Need Fail2ban? What It Catches on Ubuntu and Docker

> On a key-only server, Fail2ban ignored failed logins for allowed users, and its bans missed Docker ports. What it catches, and a tested container jail.

Published: 2026-11-05. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/fail2ban-ubuntu-docker

You probably don't need Fail2ban for SSH if you use key-only login and restrict port 22 at a firewall.
In my tests on key-only Ubuntu 24.04, its default jail ignored wrong-key attempts for any username `sshd` allows, and its bans didn't touch published Docker ports.
It's more useful for a container with a login page.

I tested this on Ubuntu 24.04 and 26.04 in systemd-booted containers under WSL2, with a second container on the same network as the attacker.
The Docker jail ran on both releases.

## What Does Fail2ban Do on Ubuntu by Default?

It installs, starts, and protects SSH with one jail:

```bash
sudo apt install fail2ban
sudo fail2ban-client status sshd
```

Ubuntu 24.04 ships version `1.0.2`, and 26.04 ships `1.1.0`.
On 24.04, Ubuntu's defaults live in `/etc/fail2ban/jail.d/defaults-debian.conf`:

```ini
[DEFAULT]
banaction = nftables
banaction_allports = nftables[type=allports]
backend = systemd

[sshd]
enabled = true
```

The jail bans a client for `10` minutes after `5` failures within `10` minutes.
It reads from the systemd journal, so it works without `/var/log/auth.log`, which didn't exist on my test machine.

## What Does It Actually Catch?

Far less than you'd expect once passwords are off.
Here is every kind of attempt I tried on 24.04, and whether the default jail counted it.
All but the first row had passwords disabled:

| Attempt | What sshd logged | Counted by the jail |
| --- | --- | --- |
| Wrong password, passwords enabled | Failed password for deploy | Yes, banned after 5 |
| Wrong key, real user | Connection closed by authenticating user deploy | No, 0 of 6 |
| Wrong key, root | Connection closed by authenticating user root | No, 0 of 6 |
| Wrong key, root, with PermitRootLogin no | Connection closed by authenticating user root | No, 0 of 6 |
| Wrong key, root, with AllowUsers deploy | User root not allowed because not listed in AllowUsers | Yes, banned after 5 |
| Password attempt, root | Connection closed by authenticating user root | No, 0 of 3 |
| Wrong key, user that does not exist | Invalid user admin | Yes, banned |
| nmap ssh2-enum-algos, normal mode | Unable to negotiate | No, 0 of 6 |
| nmap ssh2-enum-algos, aggressive mode | Unable to negotiate | Yes, banned |

So on a key-only server, it mostly bans bots guessing usernames that don't exist or aren't allowed.
Those attempts couldn't have succeeded anyway, since there's no password to guess.
`AllowUsers` is worth setting if you run Fail2ban, because it turns attempts on `root` into bans.

Fail2ban still has value there.
It cuts log noise, and `mode = aggressive` in a `[sshd]` section of `/etc/fail2ban/jail.d/sshd.local` catches scanners too.
Key-only login is what keeps attackers out, and Fail2ban adds little to that.

## Does Ubuntu 26.04 Change This?

Yes, because OpenSSH now does part of this job itself.
Ubuntu 26.04 ships OpenSSH 10.2, which has per-source penalties turned on by default:

```bash
sudo sshd -T | grep -i '^persourcepenalties'
```

```text
persourcepenalties crash:90 authfail:5 noauth:1 grace-exceeded:10 refuseconnection:10 max:600 min:15 max-sources4:65536 max-sources6:65536 ...
```

Ubuntu 24.04's OpenSSH 9.6 reports nothing for that setting.

On 26.04, wrong-key attempts against `root` still counted as `0` in Fail2ban, the same gap as 24.04.
But at one attempt per second, `sshd` answered the first 5 with `Permission denied` and dropped the next 5 on its own, and the client saw `Connection reset by peer`:

```text
drop connection #0 from [198.51.100.50]:48548 on [198.51.100.10]:22 penalty: failed authentication
```

This works as a rate limit rather than a long ban, since attempts reached `Permission denied` again moments later.
Fail2ban's bans for nonexistent users still worked at one attempt per second.
If many of your users share one address, `PerSourcePenaltyExemptList` lets you exclude it from the penalties.

## Does a Ban Protect Docker Containers?

No.
I banned my test client in the SSH jail, then tried both services from it:

| Request from the banned address | Result |
| --- | --- |
| Before the ban: SSH | Reached sshd, permission denied |
| Before the ban: published container | HTTP 200 |
| During the ban: SSH | Connection refused |
| During the ban: published container | HTTP 200 |

The rule explains why.
Fail2ban adds it to its own table for one port, and [**published container ports take a different path**](https://milanjovanovic.tech/blog/docker-bypasses-ufw):

```text
tcp dport 22 ip saddr @addr-set-sshd reject with icmp port-unreachable
```

A login form in a container gets no protection from this, even if an `sshd` ban is active for the same address.

## How Do You Ban Someone From a Container?

Point a jail at the container's logs, insert its rule into `DOCKER-USER`, and start Fail2ban after Docker.
This example bans clients that collect `5` HTTP `401` responses from an nginx container using Docker's default JSON log files.

Create `/etc/fail2ban/filter.d/docker-http-auth.conf`:

```ini
[Definition]
failregex = ^\{"log":"<HOST> - .* \\"[A-Z]+ [^"]*\\" 401 
datepattern = "time":"%%Y-%%m-%%dT%%H:%%M:%%S
```

Create `/etc/fail2ban/jail.d/docker-web.local`:

```ini
[docker-web]
enabled     = true
backend     = polling
filter      = docker-http-auth
logpath     = /var/lib/docker/containers/*/*-json.log
logtimezone = UTC
maxretry    = 5
findtime    = 10m
bantime     = 1h
chain       = DOCKER-USER
banaction   = iptables-allports[blocktype=DROP]
```

`logtimezone = UTC` is there because Docker writes these timestamps in UTC, whatever the server's time zone is.

Create `/etc/systemd/system/fail2ban.service.d/after-docker.conf`:

```ini
[Unit]
After=docker.service
Wants=docker.service
```

Reload systemd, restart Fail2ban, and send requests from another machine:

```bash
sudo systemctl daemon-reload
sudo systemctl restart fail2ban
```

```text
req 1: 401  req 2: 401  req 3: 401  req 4: 401  req 5: 401  req 6: timeout
banned client: timeout
other client: 200
```

The result was identical on 24.04 and 26.04, and the ban survived restarting Docker, restarting Fail2ban, and a reboot.
Fail2ban saw the real client address because Docker's DNAT preserves it on a bridge network, which you can confirm in the container's log line.

Each of these broke the jail in my tests, or would break it on your server:

- **Fail2ban starting before Docker.** Without the drop-in, a reboot left the jail reporting a ban with no jump in `DOCKER-USER`, and the banned client got `200`.
  The log showed `Failed to execute ban jail 'docker-web'`. With the drop-in, the ban and the jump came back on both releases.
- **A redeploy.** Fail2ban only watches log files that exist when it starts, and a recreated container gets a new log directory.
  After I recreated the container, seven `401` responses counted as `0` failures.
  Run `sudo systemctl restart fail2ban` after each deploy.
- **`chain` set in the wrong place.** My first attempt used `banaction = iptables-allports[chain=DOCKER-USER]`.
  It created a chain and reported a ban but blocked nothing, because the jump never went into `DOCKER-USER`.
- **A `ufw reload` with the [**after.rules fix**](https://milanjovanovic.tech/blog/docker-bypasses-ufw).** That block rebuilds `DOCKER-USER`, which removed the jump, and the banned client got `200` until I restarted Fail2ban.
- **A log format you didn't test.** The regex above matches the nginx format inside Docker's JSON.
  Traefik and your own app log differently.
- **Something in front of the container.** Behind Cloudflare or another proxy, the address in the log may be the proxy's, and banning it blocks everyone behind it.
  Use that provider's firewall instead.
  I only tested a bridge network, and Swarm's ingress mesh rewrites source addresses too.

The glob also watches every container's log, so a `401` from any of them counts.
I only tested these bans over IPv4.

## What Happens if You Move the SSH Port?

Two things break quietly, so I'd leave it on `22` and restrict it with a firewall instead.

Ubuntu uses socket activation for SSH on both releases, so editing the config and restarting the service doesn't move the port.
Allow the new port in UFW and your provider firewall first, because the steps below stop SSH listening on `22`:

```bash
sudo ufw allow 2222/tcp
echo 'Port 2222' | sudo tee /etc/ssh/sshd_config.d/10-port.conf
sudo systemctl restart ssh
ss -tln | grep -E ':(22|2222) '
```

```text
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:*
LISTEN 0 4096 [::]:22 [::]:*
```

You need to reload the generated socket unit:

```bash
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
```

The default jail doesn't follow the move either.
It kept blocking `tcp dport 22`, so a banned client still reached `sshd` on `2222`:

```text
banned, port 2222: deploy@198.51.100.10: Permission denied (publickey).
```

That message comes from `sshd`, so the ban did nothing on the new port.
After setting `port = 2222` in a `[sshd]` jail override, the same client got `Connection refused`.

## Is ufw limit a Simpler Option?

It's simpler, but it counts connections rather than failures.
`ufw limit 22/tcp` rejects an address after too many new connections in 30 seconds, and it can't tell you from an attacker:

```text
1: logged-in
2: logged-in
3: logged-in
4: logged-in
5: logged-in
6: ssh: connect to host 198.51.100.10 port 22: Connection refused
7: ssh: connect to host 198.51.100.10 port 22: Connection refused
8: ssh: connect to host 198.51.100.10 port 22: Connection refused
after 31s: logged-in
```

Those were all logins with a valid key.
A deployment script that opens several SSH connections in a row can hit the same limit.

## So Should You Install It?

It depends on what's exposed:

| Your setup | What I would do |
| --- | --- |
| SSH restricted by a provider firewall or VPN | Skip it for SSH, since the attempts never arrive |
| Public SSH, keys only, Ubuntu 26.04 | Optional, OpenSSH penalties already slow repeat offenders |
| Public SSH, keys only, Ubuntu 24.04 | Optional, for less log noise |
| Passwords still enabled | Turn passwords off first, then decide |
| Container with a public login form | Use the DOCKER-USER jail, or rate limit in the app or proxy |

## Summary

Fail2ban adds little to a server that already requires SSH keys.
Its default jail ignores wrong-key attempts for usernames `sshd` allows, and its SSH bans don't reach published containers.

Restrict port `22` first, so that the attempts never arrive.
Use Fail2ban where a guessed password can get in, such as a login form in a container.
That jail needs `chain = DOCKER-USER`, the systemd drop-in, and a restart after each deploy.

The rest of the list is in my [**Ubuntu server hardening checklist**](https://milanjovanovic.tech/blog/ubuntu-server-hardening), and my [**self-hosting course**](https://milanjovanovic.tech/pragmatic-self-hosting) sets up key-only SSH and private administration on a real server.

Thanks for reading.

And stay awesome!

---

## Frequently asked questions

### Do I need Fail2ban if I only use SSH keys?

Probably not for security. With passwords disabled, an attacker cannot guess their way in. In testing on Ubuntu 24.04, the default jail did not count wrong-key attempts against usernames that sshd allows, so it mostly banned names that do not exist or are not allowed. Its main benefit there is less log noise.

### Does Fail2ban protect Docker containers?

Not by default. A banned address could no longer reach SSH, but it still got HTTP 200 from a published container, because the default ban rule applies to the input path and port 22. To ban traffic to containers, use a jail with chain set to DOCKER-USER, and start Fail2ban after Docker so that it survives a reboot.

### Why is Fail2ban not banning failed SSH logins?

On a key-only server this is expected for usernames that sshd allows, because the log line for a failed key attempt does not match the default filter. Setting mode to aggressive matches more cases, such as scanners that fail key exchange. Also check that the jail port matches your SSH port.

### Does Fail2ban work on Ubuntu 24.04 and 26.04?

Yes. Ubuntu 24.04 ships Fail2ban 1.0.2 and 26.04 ships 1.1.0, and both enabled the sshd jail on install using the systemd journal and nftables. Bans worked on both in testing. Ubuntu 26.04 also has OpenSSH penalties that drop repeat offenders without Fail2ban.

### What is a good alternative to Fail2ban for SSH?

Restrict port 22 to your IP address at a provider firewall, or use a VPN so that SSH is not public at all. Then there are no attempts left to ban. The command ufw limit is a simple rate limit, but it also blocks legitimate rapid connections.
