Sys-Admin

Fail2ban SSH

Introduction

Fail2ban cuts brute-force and credential-stuffing noise on a production server by banning IP addresses that repeatedly fail SSH logins. It is one layer in a defence-in-depth stack, not a replacement for key-only authentication or a firewall.

How Fail2ban works

Fail2ban reads log files (or the systemd journal), matches failure patterns, and calls a firewall to block the source address for a set time.

  1. A service such as sshd writes a failed-login event to the auth log or journal.
  2. A filter (regular expressions in /etc/fail2ban/filter.d/) extracts the offending IP from each failure.
  3. A jail counts failures per IP. If an IP reaches maxretry failures within findtime, the jail triggers.
  4. An action (in /etc/fail2ban/action.d/) adds a firewall rule through nftables, iptables or firewalld.
  5. After bantime expires, Fail2ban removes the rule and the IP can try again.

Threat model

| Threat | Does Fail2ban help? | Primary control | ——– | ——————- | —————-| Password guessing from one IP | Yes, bans after a few failures | Disable password logins | Slow, low-rate attacks | Partly, with a long findtime and the recidive jail | Key-only authentication | Distributed botnets using many IPs | Limited, each IP stays under the threshold | Key-only authentication, network allow-listing | Stolen or leaked SSH private key | No | MFA, key passphrases, short-lived certificates | Unpatched OpenSSH or kernel vulnerability | No | Patching, minimal attack surface | Log flooding and CPU load from scanners | Yes | Fail2ban plus a firewall rate limit |

Scope of this guide

This guide targets Linux servers running OpenSSH, covering Debian/Ubuntu and RHEL-family distributions (RHEL, Rocky, AlmaLinux, Fedora). It installs Fail2ban, tunes the SSH jail, hardens sshd itself, and lists wider baseline controls for a production host.
Commands assume root or sudo access.
A mistake in SSH or firewall configuration can lock you out of a remote server, so set up a recovery path before you change anything.
[ ] A non-root user with sudo rights and a working SSH public key.
[ ] A second, already-open SSH session kept alive while you test changes.
[ ] Out-of-band access: cloud provider console, Intelligent Platform Management Interface (IPMI), or a rescue mode.
[ ] Your own admin IP addresses or VPN range noted for the ignoreip allow-list.
[ ] A recent backup or snapshot of the server.
[ ] Accurate time via chrony or systemd-timesyncd, since ban windows depend on correct timestamps.
[ ] Firewall in place (nftables, ufw or firewalld) with the SSH port allowed.

Check your environment

OS and version

cat /etc/os-release

OpenSSH version

ssh -V

Where SSH logs go: a file, or only the journal?

ls -l /var/log/auth.log /var/log/secure 2>/dev/null
sudo journalctl -u ssh -u sshd --since "10 min ago" -n 10 --no-pager

Active firewall backend

sudo nft list ruleset | head
sudo iptables -V

Important: Recent Debian releases (12 and later) no longer ship rsyslog by default, so /var/log/auth.log may not exist. Use the systemd backend in that case (see the SSH jail section).

Getting started

Install Fail2ban from your distribution’s repositories, enable the service, and confirm the default sshd jail works before customising it.

1. Installation

Debian / Ubuntu

sudo apt update
sudo apt install -y fail2ban python3-systemd

RHEL / Rocky / AlmaLinux (Fail2ban comes from EPEL)

sudo dnf install -y epel-release
sudo dnf install -y fail2ban fail2ban-firewalld

Fedora

sudo dnf install -y fail2ban

2. Enable and start

sudo systemctl enable --now fail2ban

Check status

sudo systemctl status fail2ban --no-pager

3. Understand the file layout

| Path | Purpose | Edit it? | | —- | ——- | ——– | | /etc/fail2ban/jail.conf | Packaged defaults for all jails | No, upgrades overwrite it | | /etc/fail2ban/jail.local | Your overrides | Yes | | /etc/fail2ban/jail.d/*.local | Per-jail drop-in overrides, loaded in alphabetical order | Yes | |/etc/fail2ban/filter.d/|Failure-matching regular expressions|Only to add custom filters| |/etc/fail2ban/action.d/|Ban and unban actions (nftables, firewalld, mail)| Rarely | |/var/log/fail2ban.log | Fail2ban’s own log | No |

Note: Never edit jail.conf directly. Put changes in jail.local or jail.d/ so package updates do not overwrite them.

4. Confirm the default SSH jail

On Debian and Ubuntu the sshd jail is enabled out of the box. On RHEL-family systems it is not, so enable it in the next section. Verify the state:

sudo fail2ban-client status
sudo fail2ban-client status sshd

A working jail reports Currently failed, Total failed, Currently banned and Banned IP list.

Configuring the SSH jail

Create /etc/fail2ban/jail.local with conservative defaults, an allow-list for your admin IPs, and escalating bans for repeat offenders.

Key parameters

| Parameter | Meaning | Recommended value | | ——— | ——- | —————– | | maxretry | Failures allowed within findtime before a ban | 3 to 5 | | findtime | Window in which failures are counted | 10m | | bantime | Duration of a ban | 1h for the base ban | | bantime.increment | Lengthen the ban for repeat offenders | true | | bantime.factor | Multiplier applied on each repeat ban | 2 | | bantime.maxtime | Ceiling for escalated bans | 4w | | ignoreip | Addresses never banned | Admin IPs, VPN range, 127.0.0.1/8, ::1 | | backend | How logs are read | systemd (journal) or auto (log files) | | banaction | Firewall tool used to block an IP | nftables, iptables-multiport or firewallcmd-rich-rules | | mode (sshd filter) | Strictness of failure matching | aggressive |

Example /etc/fail2ban/jail.local.

[DEFAULT]
# Never ban these (space or comma separated).
ignoreip = 127.0.0.1/8 ::1

bantime  = 1h
findtime = 10m
maxretry = 3

# Escalate bans for repeat offenders
bantime.increment = true
bantime.factor    = 2
bantime.maxtime   = 4w

# Firewall action that matches your host
banaction          = nftables
banaction_allports = nftables[type=allports]

# Email alerts: requires a working MTA
# destemail = security@example.com
# sender    = fail2ban@example.com
# action    = %(action_mwl)s

[sshd]
enabled  = true
port     = ssh
mode     = aggressive
maxretry = 3

# Debian 12+/Ubuntu with journald only:
backend  = systemd
# Servers that still write /var/log/auth.log (or /var/log/secure on RHEL):
# backend = auto
# logpath = %(sshd_log)s

If you changed the SSH port, set port = 2222 (or your port number) so the ban rule covers the correct port.

Block repeat offenders: the recidive jail.
The recidive jail watches Fail2ban’s own log and bans IPs that keep getting banned, for a long time across all ports.

[recidive]
enabled   = true
bantime   = 1w
findtime  = 1d
maxretry  = 3
banaction = nftables[type=allports]

Choose the firewall backend

| Host uses | Set banaction to | | ——— | —————— | | nftables directly (Debian 11+, Ubuntu 22.04+, RHEL 9) | nftables and nftables[type=allports] | | iptables (legacy) | iptables-multiport and iptables-allports | | firewalld (RHEL family default) | firewallcmd-rich-rules and firewallcmd-allports | | ufw | ufw |

Use only one firewall manager. Running ufw or firewalld alongside hand-written nftables rules can overwrite Fail2ban’s chains when the firewall reloads.

Note: add allowipv6 = auto to Fail2ban’s /etc/fail2ban/fail2ban.conf configuration file. Find the [Definition] section

Apply and verify

Validate configuration syntax first.

sudo fail2ban-client -t

Apply

sudo systemctl restart fail2ban

Confirm the jails are loaded and read the effective settings.

sudo fail2ban-client status
sudo fail2ban-client status sshd
sudo fail2ban-client get sshd maxretry
sudo fail2ban-client get sshd bantime

Caution: Add your own IP to ignoreip before testing. A mistyped password during testing can ban you from your own server.

Note: bantime.increment, bantime.factor and bantime.maxtime need Fail2ban 0.11 or later. Check with fail2ban-client --version.

Reference

  1. Fail2ban wiki
  2. Fail2ban RedHat