Fail2ban: Protect SSH from Brute Force Attacks on Linux

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.

Contents
  1. Prerequisites
  2. Install Fail2ban
  3. How Fail2ban Works
  4. Configuration File Layout
  5. Base Configuration in jail.local
  6. Configure the SSH Jail
  7. Monitoring Bans
  8. Manually Ban and Unban IPs
  9. Whitelist Your Own IP
  10. Fail2ban with nftables
  11. Nginx and WordPress Jails
  12. Incremental Ban Times
    1. Further Reading

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 with nano or vim

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), and bantime (how long to block).
  • Action — what happens when maxretry is 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


Go up

This site uses cookies for analytics and advertising (Google AdSense). By continuing to browse, you accept our use of cookies. Learn more