RE:NODE
Browse hosting

VDS13 min read

Fail2ban on a VDS: jails, bans and proving it works

Install fail2ban, write a jail.local that survives upgrades, tune bantime and findtime, add nginx jails, and verify the bans are real rather than theoretical.

0 readers

Fail2ban watches log files, and when the same address fails to authenticate too many times in a row it adds a firewall rule that drops it for a while. That is the whole product. Twenty minutes of setup removes most of the noise from your authentication log and a measurable amount of load from a small server, and it is one of the four or five things worth doing on the first day of a new VDS.

It is also routinely oversold. Fail2ban cannot stop a distributed attack where every attempt comes from a different address, it cannot stop a volumetric flood, and if you have already disabled password authentication on SSH then the brute-force attempts it blocks were never going to succeed anyway. Install it because it keeps your logs readable and your CPU free, not because it makes the box secure. The thing that makes SSH secure is turning passwords off, which is a different post: SSH keys and hardening.

How it actually works#

Three pieces, and understanding the split makes every problem easier to diagnose.

A filter is a set of regular expressions that recognise a failure in a specific log format. They live in /etc/fail2ban/filter.d/, one file per service, and the distribution ships dozens of them: sshd.conf, nginx-http-auth.conf, postfix.conf, and so on.

A jail ties a filter to a log source and a set of numbers: how many failures, in what window, and how long the ban lasts. Jails are defined in /etc/fail2ban/jail.conf and, importantly, overridden by you somewhere else.

An action is what happens when the jail fires. The default writes an iptables or nftables rule that rejects the address on the ports the jail covers. Actions live in /etc/fail2ban/action.d/, and there are alternatives that call ufw, send mail, or hit an API.

password attemptreadsmaxretry reacheddropped for bantimefail2banfilter plus counterFirewall ruleiptables or nftablesAttackerrepeated loginssshdwrites a failure lineLog sourcejournald or auth.log
What happens between a failed login and a ban

Two consequences follow from that diagram. Fail2ban is reactive: it reads a line after it has been written, so the failed attempts always happen first, and a ban takes effect a second or two later. And a ban is only ever a firewall rule, so anything that bypasses your firewall also bypasses fail2ban. That last point has a very common shape, covered near the end of this post.

Installing it and the only file you should edit#

bash
$ apt update && apt install fail2ban$ systemctl enable --now fail2ban$ fail2ban-client status

Do not edit jail.conf. It is a packaged file, it is replaced on every upgrade, and your changes disappear silently. Create /etc/fail2ban/jail.local instead, which is read afterwards and wins. Anything you leave out of jail.local falls back to the shipped defaults, so the file only needs to contain what you are changing.

/etc/fail2ban/jail.local
[DEFAULT]bantime  = 1hfindtime = 10mmaxretry = 5ignoreip = 127.0.0.1/8 ::1 203.0.113.42# Ban for longer each time the same address comes back.bantime.increment = truebantime.maxtime   = 5w[sshd]enabled  = truemode     = aggressivemaxretry = 4bantime  = 2h

The shipped defaults are bantime = 10m, findtime = 10m and maxretry = 5. Ten minutes is short enough to be nearly decorative against a patient scanner, which is why the first thing most people change is bantime.

bantime.increment is the setting worth knowing about and the one people miss. With it on, an address that gets itself banned again after the first ban expires is banned for twice as long, then four times, up to bantime.maxtime. A single curious visitor is out for an hour; something that keeps coming back for a week ends up out for five. It costs nothing and it does almost all of the useful work.

ignoreip is your seatbelt. Put your home or office address in it if you have a static one. If you do not, skip it rather than guessing, and read the unbanning section below instead.

The sshd jail, and the log it cannot find#

The sshd jail is enabled in most distributions' defaults and is the only one that matters on a fresh box. The modern failure is not configuration, it is the log source.

Fail2ban traditionally reads /var/log/auth.log, which is written by rsyslog. Recent minimal Debian and Ubuntu images do not install rsyslog at all, and everything goes to the systemd journal instead. On those systems the sshd jail fails to start, and the error in journalctl -u fail2ban is about a missing log path rather than about anything you did. The fix is one line:

/etc/fail2ban/jail.local
[sshd]enabled = truebackend = systemd

Check which situation you are in before you assume: ls -l /var/log/auth.log. If the file exists and is growing, the default file backend is fine. If it does not, set backend = systemd and the jail reads the journal directly.

The mode parameter on the sshd filter is worth setting. normal matches straightforward authentication failures. aggressive adds the connection-closed-before-authentication lines and the malformed-packet noise that scanners generate, which catches bots that probe without ever submitting a password. On a server with password authentication already disabled, aggressive is the mode that actually bans anything, because a bot trying passwords against a key-only daemon never produces a classic failure line.

Ban times, find times, and what the numbers mean#

Three numbers decide the behaviour and they interact in a way that is easy to get backwards.

SettingDefaultWhat it means
maxretry5Failures needed before a ban
findtime10mThe window those failures must fall inside
bantime10mHow long the firewall rule stays
bantime.incrementoffMultiply the ban each repeat offence
bantime.maxtimenoneCeiling for the incremented ban

findtime is a sliding window, not a bucket that resets on a clock. Four failures spread over eleven minutes with a ten-minute findtime never triggers anything, because at no point were there four inside a ten-minute span. A slow scanner that tries one password every three minutes is invisible to the defaults, which is a real technique and one reason to widen findtime rather than lower maxretry.

Sensible starting numbers for a server only you log into: maxretry = 4, findtime = 30m, bantime = 1h, with bantime.increment on. If other people use the box, go easier on the first offence and let the increment punish persistence, because the one person guaranteed to trip a strict jail is a colleague with an old key still in their agent.

There is a second-order jail shipped with fail2ban that is worth turning on once the rest works, and it is the one almost nobody enables:

/etc/fail2ban/jail.local
[recidive]enabled  = truebantime  = 1wfindtime = 1dmaxretry = 5

The recidive jail reads fail2ban's own log and bans addresses that got themselves banned by any other jail five times in a day. It is the catch-all for anything that comes back after its ban expires, and it covers hosts that rotate slowly between your services. It needs /var/log/fail2ban.log to exist; if your install logs to the journal instead, set backend = systemd on this jail too.

Jails for a web server#

If the VDS runs nginx, three shipped jails earn their place. Add them to jail.local, adjusting the log paths to match your setup.

/etc/fail2ban/jail.local
[nginx-http-auth]enabled = truelogpath = /var/log/nginx/error.log[nginx-botsearch]enabled = truelogpath = /var/log/nginx/access.log[nginx-limit-req]enabled = truelogpath = /var/log/nginx/error.logmaxretry = 10findtime = 1m

nginx-http-auth catches brute force against HTTP basic authentication. nginx-botsearch matches requests for the paths scanners always ask for, which on a WordPress site is a steady drip all day. nginx-limit-req is the interesting one: it does nothing unless you have configured nginx's own limit_req zones, and then it promotes a client that keeps hitting the rate limit from "throttled" to "banned at the firewall", which is much cheaper to serve.

That ordering is the general principle. Fail2ban is the blunt instrument at the end. The application should rate-limit first, because it can tell a real user from a bot in ways a log line cannot, and only the clients that ignore the throttle deserve a firewall rule. Rate limits and abuse goes through where each layer belongs.

If you use ufw for the firewall, point fail2ban at it so the two tools are not writing competing rules:

/etc/fail2ban/jail.local
[DEFAULT]banaction = ufw

Otherwise fail2ban inserts its own chains ahead of ufw's, which works, but leaves you with two places to look when an address is unexpectedly blocked. The rest of the firewall story is in the ufw firewall guide.

Proving it works#

An untested fail2ban install is a comfort object. Four checks, in order, and each one answers a different question.

bash
$ fail2ban-client status                     # which jails are running$ fail2ban-client status sshd                # counters and current bans$ fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf$ iptables -S | grep f2b                     # the rules that exist right now$ nft list ruleset | grep -i f2b             # on nftables systems

fail2ban-client status sshd prints currently failed, total failed, currently banned, total banned and the list of banned addresses. If total failed is climbing but total banned stays at zero, the filter is matching and the numbers are too loose. If total failed is stuck at zero while journalctl -u ssh clearly shows attempts, the filter is not matching the log at all, which is almost always the wrong backend or the wrong logpath.

fail2ban-regex is the tool that settles it. Point it at a real log and a filter file and it tells you how many lines matched, how many were ignored, and how many it could not parse. Add --print-all-missed to see the lines it skipped, which is how you find out that your log format has a timestamp it does not recognise.

To test end to end, fail yourself deliberately from a second machine or a phone tethered on mobile data - not from the address you are currently connected from. Get the password wrong five times, then check the status output from your existing session. Then unban yourself.

bash
$ fail2ban-client set sshd unbanip 203.0.113.9$ fail2ban-client unban 203.0.113.9        # every jail at once$ fail2ban-client unban --all              # clear everything

What fail2ban will not do for you#

Say the uncomfortable parts out loud, because a lot of guides do not.

  • Distributed attempts get through. A botnet with ten thousand addresses making three attempts each never reaches maxretry on any single address. Fail2ban is built for the single persistent scanner, which is most of the traffic, but it is not a defence against a patient attacker.
  • It does nothing about volumetric floods. Packets still arrive, still consume your uplink and still cost the kernel work. Filtering that has to happen upstream of your machine, and DDoS attacks on game servers explained covers what can and cannot be done about it.
  • Docker publishes ports past it. This is the big one. A container started with -p 5432:5432 is reached through the DOCKER-USER and FORWARD chains, and fail2ban's default rules live in INPUT, so the ban has no effect on anything in a container. Either bind the container to localhost with -p 127.0.0.1:5432:5432, or tell the action to write into the right chain with banaction = iptables-allports[chain=DOCKER-USER]. Docker on a VDS has the rest of that caveat.
  • It cannot help a weak password. Five attempts is plenty if the password is admin123. Fail2ban raises the cost of guessing; it does not raise the cost enough to matter against a genuinely bad secret.
  • It is not a substitute for turning passwords off. With PasswordAuthentication no and key-only access, the brute force it blocks was already failing. That is still worth doing for the log noise and the CPU, but be honest about the order of operations.

If something has already gone wrong rather than been prevented, fail2ban is not the tool you want - what to do when your server is hacked is.

Day-to-day operation#

After any change to jail.local, reload rather than restart, so existing bans survive:

bash
$ fail2ban-client reload          # all jails$ fail2ban-client reload sshd     # one jail$ fail2ban-client get sshd bantime$ fail2ban-client set sshd banip 198.51.100.7$ journalctl -u fail2ban -n 50 --no-pager

Bans are persisted in a small SQLite database at /var/lib/fail2ban/fail2ban.sqlite3, so they survive a restart of the service and a reboot of the machine. That is also where the increment history lives, which is why a repeat offender keeps its escalating ban across reboots.

Once a month, glance at fail2ban-client status sshd and at the size of your authentication log. A sudden change in either direction is information: a log that has gone quiet usually means rsyslog stopped or the jail broke, not that the internet became polite. The wider habit of deciding which logs to keep and for how long is in logs worth keeping, and the commands for reading them quickly are in Linux commands for server admins.

On a managed RE:NODE plan you do not install fail2ban, because there is no host shell to install it on. The equivalent protections are built in: the panel puts a captcha and rate limits on login, stores password hashes with bcrypt, offers two-factor with authenticator codes and single-use recovery codes, keeps a session list you can sign out of, and lets API keys be restricted by address. Upstream filtering drops obvious volumetric floods before they reach the node. On a VDS all of that is yours to assemble, and fail2ban is one brick of it.

FAQ#

Does fail2ban slow the server down?

Not noticeably. It is a Python process reading log lines, typically using a few tens of megabytes of memory and almost no CPU. The bans themselves are firewall rules, which the kernel evaluates in microseconds. A jail with an enormous ban list and a very short findtime on a busy web log is the only case where it shows up at all.

Why is my sshd jail reporting zero failures?

Almost always the log source. Check whether /var/log/auth.log exists; on images without rsyslog it does not, and you need backend = systemd in the jail. Confirm with fail2ban-regex against a real log, which tells you exactly how many lines it matched.

Will it ban me if I mistype my password?

Yes, and it should. That is why ignoreip exists for a static address, why you keep a second session open while changing anything, and why you check that your provider's console access works before you need it. Use keys and the question stops arising.

Is fail2ban enough on its own?

No. It is one layer on top of key-only SSH, a default-deny firewall, current packages and unprivileged service users. It removes noise and stops the lazy attacks. Every serious control is somewhere else, and the first hour on a new VDS lists them in order.

Can it protect a game server port?

Only if the game writes a parsable log of failed attempts, which most do not, and only for connection-based abuse rather than floods. Banning by address is also a poor fit for players, who share addresses and change them. Use the game's own ban list and RCON for players, and keep fail2ban for administrative interfaces.

How do I see every address currently banned?

fail2ban-client status <jail> prints the list for one jail. Recent versions also have fail2ban-client banned, which prints every jail's list at once. To see what the kernel actually enforces, iptables -S | grep f2b or nft list ruleset.


Comments

Completely anonymous: no account, no email, no cookie. We store the name you type, the text and the time - nothing else. Links are limited and markup is not rendered.

0/2000