Protect SSH against Brute-force with fail2ban
Install and configure fail2ban to automatically block SSH brute-force attacks using jails, custom ban times, allowlists, and the recidive jail.
Before you start
- ▸Root or sudo access on the target server
- ▸OpenSSH server installed and running
- ▸A firewall active (nftables, firewalld, or iptables)
- ▸Your own public IP address known so you can add it to the allowlist
Brute-force attacks against SSH are relentless. A freshly exposed server can absorb thousands of login attempts per hour from automated scanners. fail2ban watches authentication logs and temporarily bans IPs that trip a threshold of failures. It is not a replacement for key-based auth or disabling password login, but it is a practical second line of defence that stops credential stuffing cold.
How fail2ban Works
fail2ban runs as a daemon that tails log files, matches lines against regular-expression filters, and executes actions (usually a firewall rule) when a source IP crosses a failure threshold within a time window. The unit of configuration that ties a filter to an action is called a jail. The built-in sshd jail covers the most common OpenSSH log patterns.
Modern fail2ban can use nftables, iptables, or firewalld as its backend. On current distros nftables is preferred; we will confirm the backend after installation.
Install fail2ban
Debian / Ubuntu
sudo apt update && sudo apt install fail2ban -y
Fedora / RHEL 9 / Rocky Linux
sudo dnf install epel-release -y # RHEL and Rocky only
sudo dnf install fail2ban -y
Arch Linux
sudo pacman -S fail2ban
Enable and start the service:
sudo systemctl enable --now fail2ban
Create a Local Configuration File
Never edit /etc/fail2ban/jail.conf directly. It is overwritten on package upgrades. Instead, create a local override file:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Or, more surgically, create a drop-in directory file (preferred on all distros):
sudo nano /etc/fail2ban/jail.d/sshd-local.conf
Configure the SSH Jail
Paste the following into sshd-local.conf. Adjust values to suit your environment.
[DEFAULT]
# Ban duration in seconds. 1h = 3600, 24h = 86400
bantime = 3600
# Window in which maxretry failures trigger a ban
findtime = 600
# Number of failures before a ban
maxretry = 5
# Your trusted IPs / subnets — never ban these
ignoreip = 127.0.0.1/8 ::1 192.168.1.0/24
[sshd]
enabled = true
port = ssh
# On systemd-journal distros use backend = systemd
backend = systemd
maxretry = 4
bantime = 7200
Key options explained:
- bantime: How long a ban lasts. Negative values mean permanent.
- findtime: The sliding window. fail2ban counts failures inside this window.
- maxretry: Failures inside
findtimethat trigger a ban. 4–6 is reasonable for SSH. - ignoreip: Space-separated allowlist. Always include your own IP or management subnet to avoid locking yourself out.
- backend = systemd: Reads from the journal directly instead of
/var/log/auth.log. Works on Fedora/RHEL whereauth.logmay not exist.
If your SSH daemon listens on a non-standard port, update the port line:
port = 2222
Choose a Firewall Backend
fail2ban auto-detects the available firewall. You can pin it explicitly in [DEFAULT]:
# nftables (preferred on modern systems)
banaction = nftables-multiport
banaction_allports = nftables-allports
# firewalld (Fedora / RHEL)
# banaction = firewallcmd-rich-rules
# Legacy fallback
# banaction = iptables-multiport
To verify which backend is actually active after reload:
sudo fail2ban-client get sshd actions
Enable the Recidive Jail
The recidive jail watches fail2ban's own log. An IP that gets banned repeatedly earns a much longer ban. Add this block to your local config:
[recidive]
enabled = true
logpath = /var/log/fail2ban.log
banaction = %(banaction_allports)s
bantime = 604800 ; 1 week
findtime = 86400 ; within 24 hours
maxretry = 3 ; 3 bans in 24 h → 1-week ban
On systems where fail2ban logs to the journal rather than a file, enable file logging first:
sudo nano /etc/fail2ban/fail2ban.d/logging.conf
[Definition]
logtarget = /var/log/fail2ban.log
Reload and Verify
sudo systemctl restart fail2ban
sudo fail2ban-client status
You should see output listing active jails, including sshd and recidive. Then check the SSH jail specifically:
sudo fail2ban-client status sshd
Realistic output (yours will differ):
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 47
| `- Journal matches: _SYSTEMD_UNIT=sshd.service
`- Actions
|- Currently banned: 1
|- Total banned: 8
`- Banned IP list: 203.0.113.42
Reading the Logs
fail2ban logs bans, unbans, and errors to its own log file (if configured above) and to the journal:
sudo journalctl -u fail2ban -n 50 --no-pager
sudo tail -f /var/log/fail2ban.log
Grep for ban events against a specific IP:
sudo grep '203.0.113.42' /var/log/fail2ban.log
Check raw SSH authentication failures that fail2ban is reading:
sudo journalctl -u sshd --since "1 hour ago" | grep -i 'failed\|invalid'
Managing Bans Manually
Unban an IP you accidentally blocked (swap in the real address):
sudo fail2ban-client set sshd unbanip 203.0.113.42
Manually ban an IP immediately:
sudo fail2ban-client set sshd banip 198.51.100.7
List all currently banned IPs across all jails:
sudo fail2ban-client banned
Troubleshooting
- Jail shows 0 failures even though attacks are happening: The backend may be wrong. If you are on a system that uses the journal (Fedora, RHEL, Arch, modern Ubuntu), set
backend = systemdin the[sshd]block. If you rely on/var/log/auth.log, setbackend = autoorpollingand confirm the file exists and is being written. - fail2ban-client status returns "Unable to contact server": The daemon crashed, often due to a config syntax error. Run
sudo fail2ban-client -tto test configuration before restarting. - You banned your own IP: Access the server via an out-of-band console (cloud provider dashboard, physical access) and run the unban command above. Then add your IP to
ignoreip. - Bans are not sticking across reboots (nftables): fail2ban re-applies bans on startup from its database (
/var/lib/fail2ban/fail2ban.sqlite3). If bans vanish, check thatdbpurgeageinfail2ban.confis not set too low, and confirm the nftables service does not flush all rules before fail2ban starts. AdjustAfter=in the fail2ban unit file if needed. - Recidive jail log not found: Confirm
/var/log/fail2ban.logexists and fail2ban is writing to it. The logging drop-in above fixes this on most systemd-journal-only distros.
Frequently asked questions
- Will fail2ban stop all SSH brute-force attacks?
- No. It significantly raises the cost of attacks but is not a complete solution. Disable password authentication and use SSH keys as the primary control; fail2ban is a complementary layer.
- What is the difference between bantime, findtime, and maxretry?
- findtime is the sliding window in seconds. maxretry is how many failures within that window trigger a ban. bantime is how long the resulting ban lasts.
- How do I make a ban permanent?
- Set bantime to a negative value, for example bantime = -1. fail2ban will insert a permanent firewall rule and never automatically remove it.
- Does fail2ban work if I changed my SSH port?
- Yes. Set port = 2222 (or whatever port you use) in the [sshd] jail block. fail2ban applies the ban rule to that specific port rather than the default 22.
- Can fail2ban protect services other than SSH?
- Yes. fail2ban ships with filters for Nginx, Apache, Postfix, and dozens of other services. Enable additional jails in your local config following the same pattern as the sshd jail.
Related guides
Manage Secrets with Ansible Vault
Encrypt Ansible secrets with AES-256 using ansible-vault: encrypt files and inline vars, automate with password files, and isolate group-level secrets with vault IDs.
AppArmor Explained
Learn how AppArmor profiles work, how to switch between enforce and complain mode, create new profiles, and diagnose access denials on Ubuntu, Debian, and Arch.
Apply CIS Benchmarks with OpenSCAP
Use OpenSCAP and scap-security-guide to evaluate, report on, and remediate Linux systems against CIS Benchmarks — covering install, eval, and automation.
How to Audit a Linux System with auditd
Set up auditd on Linux to track file access, syscalls, and privilege use. Covers persistent rules, file watches, ausearch, and aureport across major distros.