Fail2ban: Protect SSH from Brute Force Attacks on Linux

Tested on: Ubuntu 26.04 LTS · Debian 12 · Rocky Linux 10 — Last updated: June 2026
Fail2ban is a log-monitoring daemon that bans IP addresses exhibiting brute-force behavior. It watches log files for failure patterns, then fires iptables or nftables rules to drop traffic from offending IPs after a configurable threshold is crossed. For any internet-facing SSH server, it's the highest-leverage hardening step you can take in under ten minutes.
Prerequisites
- Root or sudo access on a Linux server running Ubuntu 26.04, Debian 12, or Rocky Linux 10
- SSH access to the server (you're hardening this — don't lock yourself out)
- Your current public IP address on hand (
curl ifconfig.me) — needed for whitelisting before any testing - Basic familiarity with systemd (
systemctl) and editing files withnanoorvim
Install Fail2ban
Install from the distribution's default repositories. The packages are current enough for production use on all three target distros.
# Ubuntu / Debian
sudo apt update && sudo apt install fail2ban -y
# Rocky Linux 10 / RHEL / Fedora (EPEL required on Rocky)
sudo dnf install epel-release -y
sudo dnf install fail2ban -y
# Arch Linux
sudo pacman -S fail2ban
# Enable and start the daemon
sudo systemctl enable --now fail2ban
# Confirm it's running
sudo systemctl status fail2ban
Expected output from systemctl status:
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/lib/systemd/system/fail2ban.service; enabled)
Active: active (running) since Mon 2026-06-02 14:22:01 UTC; 5s ago
# Verify the client can talk to the daemon
sudo fail2ban-client status
# Status
# |- Number of jail: 1
# `- Jail list: sshd
How Fail2ban Works
Understanding the internals saves time when debugging. Three components interact for every jail:
- Filter — a regex defined in
/etc/fail2ban/filter.d/that matches failure lines in a log file. Each match captures the source IP via the<HOST>placeholder. - Jail — combines a filter with a log path and three timing parameters:
findtime(the sliding window),maxretry(failure threshold within that window), andbantime(how long to block). - Action — what happens when
maxretryis exceeded. The default action adds a DROP rule to iptables or nftables. Optional actions send email notifications.
# The lifecycle for a single ban:
# 1. Auth failure → /var/log/auth.log or journald
# 2. Fail2ban filter regex matches → IP noted with timestamp
# 3. IP hits maxretry within findtime → jail triggers action
# 4. iptables/nftables DROP rule created for that IP
# 5. After bantime elapses → rule removed automatically
# Default values shipped in jail.conf (too lenient — we'll fix them):
# bantime = 10m
# findtime = 10m
# maxretry = 5
Configuration File Layout
The single most important rule: never edit .conf files directly. Package updates overwrite them. Always use .local counterparts, which are merged at runtime and survive upgrades.
/etc/fail2ban/
├── fail2ban.conf # global daemon settings — do not edit
├── fail2ban.local # your global overrides — create this if needed
├── jail.conf # all jail definitions — do not edit
├── jail.local # your jail overrides — this is your main config file
├── jail.d/ # per-service jail snippets (alternative to jail.local)
│ └── defaults-debian.conf
├── filter.d/ # regex filter definitions per service
│ ├── sshd.conf
│ ├── nginx-http-auth.conf
│ └── ...
└── action.d/ # firewall and notification actions
├── iptables-multiport.conf
├── nftables-multiport.conf
└── sendmail-whois-lines.conf
Base Configuration in jail.local
Create /etc/fail2ban/jail.local from scratch. This file holds your global defaults and all jail definitions.
sudo nano /etc/fail2ban/jail.local
[DEFAULT]
# Ban for 1 hour — the 10-minute default is far too short.
# Bots retry immediately after it expires.
bantime = 1h
# Sliding window for counting failures
findtime = 10m
# Failures within findtime before ban
maxretry = 5
# Incremental bans: each repeat offender gets double the ban time
bantime.increment = true
bantime.multiplier = 2
# Hard cap on incremental bans (1 week max)
bantime.maxtime = 1w
# CRITICAL: whitelist your own IPs before testing anything
# Replace with your actual public IP(s)
ignoreip = 127.0.0.1/8 ::1 YOUR.HOME.IP.HERE
# Use systemd journal on modern distros (Ubuntu 26.04, Debian 12, Rocky 9)
backend = systemd
# Email settings (configure your MTA separately)
destemail = root@localhost
sender = fail2ban@localhost
mta = sendmail
# Validate the config before reloading
sudo fail2ban-client -t
# OK: configuration test is successful
sudo fail2ban-client reload
Configure the SSH Jail
On Ubuntu and Debian, the sshd jail is enabled by default via jail.d/defaults-debian.conf. On Rocky Linux, you must explicitly enable it. Either way, add your own overrides in jail.local to enforce stricter settings.
# Append to /etc/fail2ban/jail.local
[sshd]
enabled = true
# Port — change if you run SSH on a non-standard port
port = ssh
# port = 2222
# Log source — %(sshd_log)s resolves to /var/log/auth.log on Debian/Ubuntu
# or journald on systemd-journal backends
logpath = %(sshd_log)s
backend = %(sshd_backend)s
# Stricter than the [DEFAULT] above — SSH deserves extra scrutiny
maxretry = 3
findtime = 5m
bantime = 24h
sudo fail2ban-client reload
# Confirm the jail is active and check its counters
sudo fail2ban-client status sshd
Sample output after some time in production:
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 1483
`- Actions
|- Currently banned: 4
`- Banned IP list: 203.0.113.1 198.51.100.42 192.0.2.99 198.51.100.200
Monitoring Bans
# List all active jails
sudo fail2ban-client status
# Detailed status for a specific jail
sudo fail2ban-client status sshd
sudo fail2ban-client status nginx-http-auth
# Follow the fail2ban log in real time
sudo journalctl -u fail2ban -f
# Count total bans since installation
sudo grep -c "Ban " /var/log/fail2ban.log
# View the actual firewall rules fail2ban created (iptables)
sudo iptables -L f2b-sshd -n --line-numbers
# View rules if using nftables
sudo nft list table inet f2b-table
# Find which IPs have been banned most (top offenders)
sudo grep "Ban " /var/log/fail2ban.log | awk '{print $NF}' | sort | uniq -c | sort -rn | head -20
Manually Ban and Unban IPs
# Ban an IP immediately in a specific jail
sudo fail2ban-client set sshd banip 203.0.113.5
# Unban a specific IP from a jail
sudo fail2ban-client set sshd unbanip 203.0.113.5
# Unban across all jails at once
sudo fail2ban-client unban 203.0.113.5
# Unban everything in a jail (useful after a misconfiguration)
sudo fail2ban-client set sshd unbanip --all
# Check whether an IP is currently banned
sudo fail2ban-client status sshd | grep 203.0.113.5
# Permanently ban an IP (bantime = -1 means no expiry)
# Set in jail.local for the jail, then manually ban:
# bantime = -1
sudo fail2ban-client set sshd banip 203.0.113.99
Whitelist Your Own IP
Do this before stress-testing the configuration. Triggering the threshold from your own IP will lock you out immediately, and recovery requires console access or a cloud provider's out-of-band terminal.
# Find your current public IP
curl -s ifconfig.me
# 203.0.113.100
# Add it to jail.local [DEFAULT]
# ignoreip = 127.0.0.1/8 ::1 203.0.113.100
# Multiple IPs or a CIDR range (e.g., your office network)
# ignoreip = 127.0.0.1/8 ::1 203.0.113.100 198.51.100.0/24
sudo fail2ban-client reload
# Verify the ignoreip list is applied to the sshd jail
sudo fail2ban-client get sshd ignoreip
Fail2ban with nftables
Ubuntu 26.04 and Debian 12 default to nftables. Fail2ban ships nftables actions but doesn't always auto-detect them. Set the action explicitly to avoid silent fallback to iptables.
# In /etc/fail2ban/jail.local [DEFAULT]:
banaction = nftables-multiport
banaction_allports = nftables-allports
sudo fail2ban-client reload
# Confirm nftables rules are being created after a ban
sudo nft list table inet f2b-table
Expected nft output with active bans:
table inet f2b-table {
set addr-set-sshd {
type ipv4_addr
elements = { 203.0.113.5, 198.51.100.42 }
}
chain f2b-chain {
type filter hook input priority -1; policy accept;
ip saddr @addr-set-sshd drop
}
}
Nginx and WordPress Jails
SSH is the priority, but HTTP services face the same automated scanning. Add these jails to jail.local.
# Nginx: block IPs hammering rate-limited endpoints
[nginx-limit-req]
enabled = true
port = http,https
logpath = %(nginx_error_log)s
maxretry = 10
findtime = 1m
bantime = 1h
# Nginx: block aggressive 404/probe scanning
[nginx-botsearch]
enabled = true
port = http,https
logpath = %(nginx_access_log)s
maxretry = 2
findtime = 30s
bantime = 2h
# Nginx: block HTTP basic auth brute force
[nginx-http-auth]
enabled = true
port = http,https
logpath = %(nginx_error_log)s
maxretry = 6
bantime = 1h
For WordPress, the built-in filters don't cover wp-login.php and xmlrpc.php abuse. Create a custom filter:
sudo nano /etc/fail2ban/filter.d/wordpress.conf
[Definition]
failregex = ^ .* "POST .*wp-login.php
^ .* "POST .*xmlrpc.php
ignoreregex =
# Test the filter against your actual Nginx access log
sudo fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress.conf
# Lines: 24342 lines, 0 ignored, 187 matched, 24155 missed
# Add to jail.local
[wordpress]
enabled = true
filter = wordpress
logpath = /var/log/nginx/access.log
port = http,https
maxretry = 5
findtime = 5m
bantime = 24h
sudo fail2ban-client reload
sudo fail2ban-client status wordpress
Incremental Ban Times
The bantime.increment feature, added in Fail2ban 0.11, is one of the most effective settings for dealing with persistent attackers. An IP banned once for 1 hour, returns, and gets banned for 2 hours. Then 4, then 8 — up to your configured maximum.
# In jail.local [DEFAULT] — already shown above, highlighted here for clarity
bantime.increment = true
bantime.multiplier = 2
bantime.maxtime = 1w
# bantime.overalljails = true # count bans across all jails for the same IP
This configuration alone removes most repeat offenders from your logs within days. The
