Tutorials  /  Linux Basics

Harden SSH Against Brute Force with fail2ban

LLudwig · August 2026 ·10 min read ·Linux Basics, Tutorial

Any SSH server reachable from the internet collects failed login attempts within minutes of going online. Most of them come from automated scanners cycling through common usernames and passwords. fail2ban reads the authentication log, counts those failures per source address and drops the offender into a firewall rule before the attempts add up to anything.

This tutorial covers installing fail2ban, writing a jail.local for the sshd jail, enabling incremental ban times, and verifying that bans actually reach the firewall.

What is fail2ban?

fail2ban is a log-parsing daemon that matches failed authentication attempts against filter regexes and inserts a temporary firewall rule for every source IP address that exceeds a configured failure threshold within a time window. It ships with filters for OpenSSH, Postfix, Dovecot, nginx and around 100 other services.

fail2ban does not replace key-based authentication or a firewall. It reduces noise and stops password-guessing loops, and it is one layer in a set that also includes PasswordAuthentication no, a restricted AllowUsers list and a default-deny firewall policy.

Prerequisites

  • A Linux server with root or sudo access. The commands below are shown for Ubuntu 24.04 and Debian 12 (fail2ban 1.0.2) and for RHEL-compatible distributions such as AlmaLinux 9. A scalable Cloud VM from centron is a suitable test target.
  • A running OpenSSH server.
  • A second SSH session that stays open for the whole procedure. A misconfigured jail can ban your own address, and an already-established session gives you a way back in.
  • Knowledge of the public IP address or network you connect from.

Install fail2ban

On Debian and Ubuntu the package is in the main repository:

Console
$ sudo apt update
$ sudo apt install fail2ban

On AlmaLinux, Rocky Linux and RHEL 9, fail2ban comes from EPEL:

Console
$ sudo dnf install epel-release
$ sudo dnf install fail2ban fail2ban-firewalld

Enable and start the service:

Console
$ sudo systemctl enable --now fail2ban
$ systemctl status fail2ban --no-pager

Check the version before you copy configuration snippets from elsewhere. Options such as bantime.increment require fail2ban 0.11 or newer:

Console
$ fail2ban-client version
1.0.2
VM

Matching infrastructure at centron

No hardware needed to follow along: ccloud³ VMs with full root access, billed by the hour and ready in seconds. Rent a cloud server →

How does fail2ban decide to ban an IP?

fail2ban bans an IP address when the number of log lines matching a jail's failregex reaches maxretry inside the findtime window, and it then keeps the firewall rule in place for bantime seconds. All three values are per jail, so the sshd jail can be stricter than a mail jail on the same host.

The path from a failed login to a firewall rule looks like this:

graph TD
  A["Failed SSH login"] --> B["journald or /var/log/auth.log"]
  B --> C["Filter /etc/fail2ban/filter.d/sshd.conf"]
  C -->|"failregex matches"| D["Failure counter per IP"]
  C -->|"no match"| E["Line ignored"]
  D --> F{"maxretry reached within findtime?"}
  F -->|"no"| G["Counter expires after findtime"]
  F -->|"yes"| H["banaction inserts firewall rule"]
  H --> I["Rule removed after bantime"]

Addresses listed in ignoreip never reach the counter stage, which is what keeps your own management network out of the jail.

Configure the sshd jail

Never edit /etc/fail2ban/jail.conf. Package upgrades overwrite it. Put your settings in /etc/fail2ban/jail.local, which is read last and wins:

Console
$ sudo nano /etc/fail2ban/jail.local

A working configuration for a public-facing SSH server:

ini
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24
bantime  = 1h
findtime = 10m
maxretry = 5
bantime.increment = true
bantime.factor    = 2
bantime.maxtime   = 1w

[sshd]
enabled  = true
backend  = systemd
port     = ssh
maxretry = 4
findtime = 10m
bantime  = 1h

What the individual keys do:

  • ignoreip — space-separated list of addresses and CIDR ranges that are never banned. Replace 203.0.113.0/24 with your own office or VPN range.
  • findtime — the sliding window in which failures are counted. 10m means five failures spread over an hour do not trigger a ban.
  • maxretry — failures required inside findtime before the ban action runs.
  • bantime — how long the firewall rule stays in place. A negative value (-1) bans permanently.
  • backend — where the jail reads its log data from. systemd reads the journal directly and is the correct choice on hosts without rsyslog.
  • port — the port the ban rule covers. If SSH listens on a non-standard port, write the number here, for example port = 2222.

Enable incremental ban times

bantime.increment = true multiplies the ban duration by bantime.factor each time the same address is banned again, up to bantime.maxtime. With the values above, a host is banned for 1 hour, then 2 hours, then 4 hours, capped at one week. This punishes persistent scanners without permanently blacklisting an address that a legitimate user may later be assigned.

Choose the right ban action

The default banaction is iptables-multiport. On Debian 12 and Ubuntu 24.04 the iptables command is a compatibility wrapper around nftables, so this works out of the box. If you manage rules with nftables natively, or with firewalld, set the matching action in the [DEFAULT] section:

ini
# Native nftables
banaction = nftables-multiport
# firewalld (RHEL-compatible systems)
banaction = firewallcmd-rich-rules

Reload fail2ban so it reads the new file:

Console
$ sudo fail2ban-client reload

Verify the jail is active

Ask fail2ban which jails it loaded and what the sshd jail currently sees:

Console
$ sudo fail2ban-client status
$ sudo fail2ban-client status sshd

Expected output for an active jail:

text
Status for the jail: sshd
|- Filter
|  |- Currently failed: 2
|  |- Total failed:     47
|  `- Journal matches:  _SYSTEMD_UNIT=sshd.service + _COMM=sshd
`- Actions
   |- Currently banned: 1
   |- Total banned:     6
   `- Banned IP list:   198.51.100.24

If Total failed stays at 0 on a server that has been reachable for a while, the filter is not matching. Test the filter against the log source directly:

Console
$ sudo fail2ban-regex systemd-journal /etc/fail2ban/filter.d/sshd.conf

The summary at the end reports how many lines were matched. A Lines: ... 0 matched result means the backend or the filter is wrong, not that no attacks happened.

Confirm the ban reached the firewall. For the iptables action:

Console
$ sudo iptables -S f2b-sshd

For the nftables action:

Console
$ sudo nft list set inet f2b-table addr-set-sshd

Bans and unbans are also written to /var/log/fail2ban.log, which is useful when you want to correlate blocks with monitoring data or forward the events to a central log collector.

Unban an address

Remove a single address from a jail:

Console
$ sudo fail2ban-client set sshd unbanip 198.51.100.24

Clear every ban in a jail at once:

Console
$ sudo fail2ban-client unban --all

List the addresses a jail currently holds (fail2ban 0.10 and newer):

Console
$ sudo fail2ban-client get sshd banned

An unban only removes the firewall rule. If the source keeps trying, it will be banned again on the next threshold breach, and with bantime.increment enabled the next ban is longer.

Add a recidive jail for repeat offenders

The recidive jail watches /var/log/fail2ban.log itself and applies a long ban to hosts that have already been banned several times by other jails. Append it to /etc/fail2ban/jail.local:

ini
[recidive]
enabled  = true
logpath  = /var/log/fail2ban.log
banaction = %(banaction_allports)s
findtime = 1d
bantime  = 4w
maxretry = 5

This blocks an address on all ports for four weeks once it has collected five bans within a day. The jail depends on fail2ban writing its own log file. On installations that log only to the journal, set logtarget = /var/log/fail2ban.log in /etc/fail2ban/fail2ban.local first.

Console
$ sudo fail2ban-client reload
$ sudo fail2ban-client status recidive

Troubleshooting

The jail fails to start with Failed during configuration: Have not found any log file. Ubuntu 24.04 server images do not always install rsyslog, so /var/log/auth.log does not exist. Set backend = systemd in the [sshd] section, or install rsyslog if you prefer file-based logs.

Bans are counted but connections still get through. The banaction does not match the firewall in use. Check journalctl -u fail2ban -n 50 for iptables or nft errors, then set banaction to nftables-multiport or firewallcmd-rich-rules as described above.

You banned your own address. Use an existing session, the provider console or an out-of-band connection and run fail2ban-client set sshd unbanip <YOUR_IP>. Then add your network to ignoreip and reload. Existing connections are not terminated by a ban, which is why keeping a second session open matters.

Changes in jail.local appear to have no effect. Confirm which values fail2ban actually loaded:

Console
$ sudo fail2ban-client get sshd maxretry
$ sudo fail2ban-client get sshd bantime

Files in /etc/fail2ban/jail.d/ are read after jail.local. On Debian and Ubuntu, jail.d/defaults-debian.conf enables the sshd jail and can override settings you expected to win.

Wrap-up

The sshd jail now blocks any address that fails four logins within ten minutes, and repeat offenders accumulate longer bans automatically. Verify the setup with fail2ban-client status sshd after a day of uptime. The counters tell you whether the filter matches real traffic.

fail2ban handles the noise, not the root cause. Disable password authentication in /etc/ssh/sshd_config with PasswordAuthentication no, restrict logins to specific accounts with AllowUsers, and keep the ban thresholds strict enough that a scanner never gets more than a handful of attempts.

Jetzt 200 € Guthaben sichern

Testen Sie Ihr Setup auf ccloud³

Registrieren Sie sich in der ccloud³ und erhalten Sie 200 € Startguthaben für Ihr Projekt – z. B. für eine PostgreSQL-VM mit automatischen Backups.

Ludwig Technische Redaktion

Schreibt bei centron über Linux-Administration, Container und Datenbanken – mit Fokus auf Anleitungen, die im Betrieb tatsächlich funktionieren.

Kategorie Linux Basics
Teilen
Noch offene Fragen?

Our team will help you with your specific setup - in German or English, by people who run the platform themselves.

War dieses Tutorial hilfreich?

Your answer is stored anonymously and helps us improve our tutorials.

Kommentare

No comments yet - be the first to ask a question about this tutorial.

Sign in to comment

Comments are open to centron customers. Sign in to your account to ask a question about this tutorial.

Weiterlesen

Das könnte Sie auch interessieren

Jetzt kostenlos anfangen

Melden Sie sich an und erhalten Sie in den ersten 60 Tagen ein Guthaben von 200 € bei centron.

Dieses Werbeangebot gilt nur für neue Konten. Angebot ausschließlich für Gewerbetreibende.

Jetzt loslegen Sales kontaktieren