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

devopsdockersecurityself-hosting
Do You Need Fail2ban? What It Catches on Ubuntu and 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:

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:

[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:

AttemptWhat sshd loggedCounted by the jail
Wrong password, passwords enabledFailed password for deployYes, banned after 5
Wrong key, real userConnection closed by authenticating user deployNo, 0 of 6
Wrong key, rootConnection closed by authenticating user rootNo, 0 of 6
Wrong key, root, with PermitRootLogin noConnection closed by authenticating user rootNo, 0 of 6
Wrong key, root, with AllowUsers deployUser root not allowed because not listed in AllowUsersYes, banned after 5
Password attempt, rootConnection closed by authenticating user rootNo, 0 of 3
Wrong key, user that does not existInvalid user adminYes, banned
nmap ssh2-enum-algos, normal modeUnable to negotiateNo, 0 of 6
nmap ssh2-enum-algos, aggressive modeUnable to negotiateYes, 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:

sudo sshd -T | grep -i '^persourcepenalties'
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:

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 addressResult
Before the ban: SSHReached sshd, permission denied
Before the ban: published containerHTTP 200
During the ban: SSHConnection refused
During the ban: published containerHTTP 200

The rule explains why. Fail2ban adds it to its own table for one port, and published container ports take a different path:

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:

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

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

[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:

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

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

sudo systemctl daemon-reload
sudo systemctl restart fail2ban
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. 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:

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) '
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:*
LISTEN 0 4096 [::]:22 [::]:*

You need to reload the generated socket unit:

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:

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:

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 setupWhat I would do
SSH restricted by a provider firewall or VPNSkip it for SSH, since the attempts never arrive
Public SSH, keys only, Ubuntu 26.04Optional, OpenSSH penalties already slow repeat offenders
Public SSH, keys only, Ubuntu 24.04Optional, for less log noise
Passwords still enabledTurn passwords off first, then decide
Container with a public login formUse 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, and my self-hosting course 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.

Browse all Self-Hosting guides →
Loading comments...

Whenever you're ready, there are 5 ways I can help you:

  1. Pragmatic Self-Hosting: Learn to deploy and run your apps on a VPS with Dokploy and Tailscale. Keep administration private, automate releases, and practice restoring backups, with source code and a runbook you can reuse.
  2. Pragmatic Clean Architecture: Join 5,000+ students in this comprehensive course that will teach you the system I use to ship production-ready applications using Clean Architecture. Learn how to apply the best practices of modern software architecture.
  3. Modular Monolith Architecture: Join 2,800+ engineers in this in-depth course that will transform the way you build modern systems. You will learn the best practices for applying the Modular Monolith architecture in a real-world scenario.
  4. Pragmatic REST APIs: Join 1,900+ students in this course that will teach you how to build production-ready REST APIs using the latest ASP.NET Core features and best practices. It includes a fully functional UI application that we'll integrate with the REST API.
  5. Patreon Community: Join a community of 5,000+ engineers and software architects. You will also unlock access to the source code I use in my YouTube videos, early access to future videos, and exclusive discounts for my courses.

The .NET Weekly

One practical .NET tip, every Saturday.

Join 66,000+ engineers who read it. Free, and you can unsubscribe any time.