Practice

Fail2ban: A Practical Way to Quiet the Bots Hammering Your Server

Every server on the internet gets probed within minutes of going online. Fail2ban reads your logs and bans the addresses doing it. Installation, the SSH jail, a custom jail for .env scans, and how to avoid banning yourself or your own uptime monitor.

Open the logs on any server that has a public address and you will find them: login attempts with usernames you have never heard of, requests for /.env, /.git/config, /wp-login.php on a server that has never run WordPress, and a slow drip of 404s that walk through every admin panel ever shipped. This starts within minutes of the server going online and never stops.

Almost none of it is aimed at you. It is automated scanning, thousands of scripts checking whether you left a key under the doormat. Most of the time you did not, and the noise is just noise. The one time you did, the script finds it first.

Fail2ban is the cheapest, most effective way I know to make most of that traffic go away.

What it does, and what it does not

Fail2ban is a small Python daemon that reads log files, matches lines against patterns, and bans the source address at the firewall when an address matches too often. Five failed SSH logins in ten minutes, and that address is blocked for a while. Ten 404s in a minute from one client, same treatment. It ships with filters for SSH, Nginx, Apache, Postfix, Dovecot and a long list of other services, and you can write your own for anything that writes a log.

It has limits, and they are worth being clear about. It works on the source address, so a distributed attack where every attempt comes from a different machine sails past it. It does nothing about a weak password or an unpatched service, since the one attempt that succeeds looks exactly like a legitimate login. It is a way to shut the door on the persistent and the clumsy, which describes almost all of the traffic in those logs.

For a few minutes of setup, that is a good deal.

Installing on Ubuntu 24.04

It is in the standard repository:

sudo apt update && sudo apt upgrade -y
sudo apt install fail2ban -y

The service starts on its own. Check that the daemon is up:

sudo fail2ban-client ping

You should get PONG back.

Fail2ban bans by adding firewall rules, through iptables or nftables underneath. If you use UFW, make sure SSH is allowed before you enable it, or the first thing the firewall does is lock you out:

sudo ufw allow OpenSSH
sudo ufw enable

Fail2ban detects UFW and works alongside it. Without UFW it talks to iptables directly. Either way there is nothing else to wire up.

The SSH jail

Configuration lives in /etc/fail2ban/jail.conf, which the package will overwrite on upgrade, so never edit it. Put your changes in jail.local, which is read on top of it. Copying the whole file is the usual starting point, because it doubles as documentation:

sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
sudo nano /etc/fail2ban/jail.local

A jail is one set of rules for one service: which log to read, which filter to apply, how many matches trigger a ban, and for how long. The SSH jail is already defined; you just need to turn it on and decide how patient to be:

[sshd]
enabled  = true
port     = ssh
maxretry = 5
findtime = 10m
bantime  = 1h

maxretry failures inside findtime earn a ban of bantime. The defaults ban for ten minutes, which the scripts barely notice. An hour is a better starting point, a day is reasonable for a server nobody legitimately logs into from unknown addresses, and bantime = -1 bans permanently. Permanent bans accumulate in the firewall forever, so prefer a long finite one.

If you only log in with keys, which you should, there is no reason to allow five password attempts at all. maxretry = 2 bans on the second wrong password and nobody legitimate will ever notice.

One thing that has changed in 24.04: Ubuntu no longer installs rsyslog by default, so /var/log/auth.log may not exist. The packaged SSH jail reads the systemd journal instead, so leave logpath out of your [sshd] section unless you have installed rsyslog yourself. If bans never happen and fail2ban-client status sshd shows zero matches, this is the first thing to check.

Restart to apply:

sudo systemctl restart fail2ban

Whitelist yourself first

The [DEFAULT] section has an ignoreip setting. Put your office and home addresses in it before you tighten anything else:

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24

Everybody who runs fail2ban has banned themselves at least once, usually while testing a new filter, usually from the only network they have access from. The whitelist is how you make that a one-time lesson.

A custom jail for .env scans

The packaged filters cover the classic services. The requests for /.env are an application-level problem, and there is no legitimate reason for a public visitor to ask for that file, so a single request is enough evidence to ban on.

Filters live in /etc/fail2ban/filter.d/. Create one:

sudo nano /etc/fail2ban/filter.d/custom-env.conf
[Definition]
failregex = ^<HOST> - .*"(?:GET|POST|HEAD) /\.env
ignoreregex =

<HOST> is fail2ban's placeholder for the address it should ban, and an Nginx access log line starts with exactly that. The rest matches the request. The backslash before .env is there because a bare dot matches anything in a regular expression. Adjust the pattern to your log format, and test it against a real log before you trust it:

sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/custom-env.conf

That prints how many lines matched. If the answer is zero and you know the requests are in there, the pattern is wrong.

Then the jail, in jail.local or in its own file under /etc/fail2ban/jail.d/:

[nginx-env-scan]
enabled  = true
filter   = custom-env
logpath  = /var/log/nginx/access.log
port     = http,https
maxretry = 1
findtime = 1m
bantime  = 1d

maxretry = 1 means one match bans, for a day, on both web ports. For a URL that a real visitor might plausibly hit by accident, an /admin path for instance, use something gentler, like three matches in five minutes.

Restart fail2ban and the next script that asks for /.env is gone before its second request.

The same shape works for wp-login.php on a non-WordPress server, for xmlrpc.php, for bursts of 404s from one address, and for anything else in your logs that only a scanner would do. There are community filter collections for the common ones. Resist the urge to write forty rules. Three or four high-signal jails remove most of the noise. Hundreds of hair-trigger rules mostly produce a denial of service you built yourself, one legitimate user at a time.

Do not ban your own monitor

This is the part I care about most, for obvious reasons.

An uptime monitor is, from your server's point of view, one address making the same request every thirty seconds, forever. That is exactly the shape of behaviour a badly tuned jail exists to punish. A filter that bans on repeated 404s will ban your monitor the day a deploy breaks the monitored route. A filter on request rate will ban it on principle. Once it is banned, every check times out or is refused, the monitor reports the site down, and you spend an hour looking for an outage that only exists for one IP address.

Two things prevent that. Barkme's checks come from a fixed prober address, shown under Settings and Team as "Monitoring Prober". Put it in ignoreip alongside your own addresses. And every check carries a Referer header starting with Barkme-, so a filter on web logs can exclude those lines outright:

ignoreregex = "Barkme-

If a monitor ever reports a site down while visitors are fine, a fail2ban ban is high on the list of things to check. The status code post has the full list of "only the monitor is affected" causes.

Watching it work

fail2ban-client is the interface to the running daemon.

An overview of every jail:

sudo fail2ban-client status

One jail in detail, with the addresses currently banned:

sudo fail2ban-client status sshd

Every ban and unban is written to /var/log/fail2ban.log, and tailing it for a few minutes after enabling a new jail tells you quickly whether it is doing what you meant:

sudo tail -f /var/log/fail2ban.log

To release an address, yours for example:

sudo fail2ban-client set sshd unbanip 203.0.113.7

And to ban one by hand without waiting for it to trip a rule:

sudo fail2ban-client set sshd banip 203.0.113.7

Use the client for these. Editing firewall rules directly works too, but then fail2ban's own record of who is banned drifts away from what the firewall is actually doing.

Five minutes, most of the noise gone

Installed, SSH jail tightened, one custom jail for the scanners, your own addresses and your monitor whitelisted. That is the whole job, and after it the logs on a public server go from a constant stream to something you can actually read.

It does nothing to replace SSH keys, updates, and a password you have never reused. It handles the part of the internet that is trying every door on the street, which is most of the traffic a small server will ever see, and it does it quietly in the background for years without attention.

An earlier version of this post appeared on my Substack in May 2025. The jail definitions above are what I run today.

Put this into practice

Barkme watches your sites around the clock and barks the moment one goes down. Free plan, no card required.

Try Barkme free