Defend Against SSH Brute‑Force Attacks with Fail2ban: Real‑World Auto‑Blocking Guide
This article explains why SSH brute‑force attacks are a real threat, describes Fail2ban's three‑layer workflow, walks through installation and basic SSH jail configuration, details key parameters, shows how to customize filters, actions, and alerts, and provides advanced tuning, troubleshooting, and production‑ready templates for robust SSH protection.
1 Background: SSH brute‑force is a real threat
Exposing SSH (port 22) to the Internet allows anyone to attempt connections. Attackers use password dictionaries and common usernames, generating 100‑1000 login attempts per day per server, many from automated tools such as Hydra, Medusa, or Burp Suite.
Source IPs are distributed (botnets or cloud servers).
Typical usernames: root, admin, test, ubuntu, centos.
Passwords are often weak or common.
Attacks can be high‑frequency bursts or low‑frequency slow attempts that evade simple thresholds.
Operators can either switch to key‑based login and disable passwords, or deploy an automated tool like Fail2ban to block malicious IPs before they succeed.
2 Fail2ban Working Principle
2.1 Core concepts
Fail2ban operates in three layers:
Log monitoring : the fail2ban-server daemon continuously watches specified log files and extracts lines that match a regular expression indicating an authentication failure.
Counter : each IP accumulates matches within a sliding window ( findtime). When the count reaches maxretry, a ban is triggered.
Action : the configured action (usually an iptables rule) is executed, adding a reject rule for the offending IP for bantime seconds.
2.2 Key parameters
maxretry : maximum failures allowed within findtime before banning (default 5).
findtime : sliding window in seconds (default 600 s, i.e., 10 min).
bantime : duration of the ban in seconds (default 600 s; negative values mean permanent ban, not recommended).
bantime.increment : enables progressive bans; each subsequent violation multiplies the ban time by multiplier^n.
2.3 Difference from traditional firewalls
Traditional firewalls have static rules that stay until manually removed. Fail2ban acts as an automated firewall rule manager, adding and removing rules based on real‑time log analysis. It also works at the application layer, knowing which IP failed authentication a specific number of times, rather than just seeing traffic on port 22.
3 Installation and basic configuration
3.1 Install Fail2ban
# CentOS / RHEL / AlmaLinux
yum install -y epel-release
yum install -y fail2ban fail2ban-systemd
# Ubuntu / Debian
apt-get update
apt-get install -y fail2ban
# Enable and start the service
systemctl enable fail2ban
systemctl start fail2ban
# Verify status
systemctl status fail2ban
fail2ban-client statusConfiguration files reside in /etc/fail2ban/:
/etc/fail2ban/
├── jail.conf # main config (do not edit directly)
├── jail.local # user overrides (recommended)
├── action.d/ # action scripts
├── filter.d/ # filter definitions
└── fail2ban.conf # internal Fail2ban settings3.2 Basic SSH protection
Fail2ban ships with an sshd jail. Verify it is enabled:
# Show all jails
fail2ban-client status
# Expected output (example)
# Number of jail: 1
# Jail list: sshdIf the jail is not enabled, enable it manually:
# Enable sshd jail and add local whitelist entry
fail2ban-client set sshd addignoreip 127.0.0.1/8
# Manually ban or unban an IP for testing
fail2ban-client set sshd banip <some-ip>
fail2ban-client set sshd unbanip <some-ip>3.3 jail.local detailed configuration
Never edit jail.conf directly; place overrides in jail.local so upgrades preserve custom settings.
# /etc/fail2ban/jail.local
[DEFAULT]
# Global defaults inherited by all jails
bantime = 1800 # 30 min
findtime = 600 # 10 min window
maxretry = 5
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 <jump-host-ip>
bantime.increment = true
bantime.multipliers = 1 2 4 8 16 32 64
bantime.rndtime = 60
loglevel = INFO
logtarget = /var/log/fail2ban.log
[sshd]
enabled = true
filter = sshd
# Use the appropriate log file for your distro
logpath = /var/log/auth.log # Debian/Ubuntu
#logpath = /var/log/secure # CentOS/RHEL
bantime = 1800
maxretry = 5
findtime = 600
bantime.increment = true
action = iptables-multiport[name=sshd, port="ssh", protocol=tcp]3.4 CentOS/RHEL log differences
On CentOS/RHEL the SSH authentication log is /var/log/secure; on Debian/Ubuntu it is /var/log/auth.log. Ensure the file exists; if using systemd‑journald you may need to configure rsyslog to create it.
# Verify log file existence
ls -la /var/log/auth.log /var/log/secure 2>/dev/null
# Example rsyslog rule for CentOS
auth.* /var/log/secure
systemctl restart rsyslog4 Filter rules and regular expressions
4.1 SSH filter details
Filters live in /etc/fail2ban/filter.d/. The default SSH filter is sshd.conf. The failregex defines which log lines constitute a failure. Different SSH versions and OSes have slight format variations, so the default regex may need adjustment.
4.2 Testing regular expressions
Fail2ban provides fail2ban-regex to test a regex against real logs:
# Test with actual auth log
fail2ban-regex /var/log/auth.log '^%(bkrv)s [.-]?%(bnc)s ...Failed password for .* from <HOST> ...'
# Test a custom pattern
fail2ban-regex /var/log/auth.log 'Failed password for root from 61.160.214.185 port 40893 ssh2'Example one‑liner to count offending IPs:
grep "Failed password" /var/log/auth.log | awk '{print $(NF-3)}' | sort | uniq -c | sort -rn | head -204.3 Common log formats
Typical log lines:
# Debian/Ubuntu
Jan 15 10:23:45 myserver sshd[12345]: Failed password for root from 1.2.3.4 port 54321 ssh2
# CentOS/RHEL
Jan 15 10:23:45 localhost sshd[12345]: Failed password for root from 1.2.3.4 port 54321 ssh2If the default regex does not match, create a custom filter, e.g., /etc/fail2ban/filter.d/sshd-custom.conf with an appropriate failregex.
5 Action configuration: iptables vs firewalld
5.1 iptables‑multiport action
Fail2ban uses the iptables-multiport action by default, inserting a REJECT rule into the INPUT chain.
# List Fail2ban iptables rules
iptables -L INPUT -n -v | grep -A 5 FAIL2BAN
iptables -L f2b-sshd -n -vThe REJECT rule returns an ICMP port‑unreachable or TCP reset, giving the attacker immediate feedback.
5.2 Switch to firewalld
If the server uses firewalld, change the action to firewallcmd-ipset for richer control.
# Install firewalld if not present
yum install -y firewalld
systemctl start firewalld
systemctl enable firewalld
# Open SSH service and reload
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
# Update jail.local
action = firewallcmd-ipset[name=sshd-all, port="ssh", protocol=tcp]
fail2ban-client reload
firewall-cmd --list-all | grep -i fail2ban5.3 Action script details
Actions are shell scripts located in /etc/fail2ban/action.d/. The default iptables-multiport script performs:
# Ban IP
iptables -I f2b-<name> -s <ip> -p tcp --dport <port> -j REJECT
# Unban IP
iptables -D f2b-<name> -s <ip> -p tcp --dport <port> -j REJECT
# Verify rule existence
iptables -C f2b-<name> -s <ip> -j REJECT6 Advanced configuration and tuning
6.1 Progressive banning (recidive jail)
Fail2ban includes a special recidive jail that bans IPs that have been banned multiple times, applying a longer ban.
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
banaction = iptables-allports
bantime = 604800 # 7 days
findtime = 86400 # 1 day window
maxretry = 3 # ban after 3 prior bans6.2 Custom alerts: WeChat / DingTalk integration
Fail2ban can invoke custom scripts for alerts. Below is a simplified example that sends a markdown message to a WeChat webhook.
#!/bin/bash
JAIL_NAME="$1"
IP="$2"
RETRY_COUNT="$3"
BANTIME_SEC="$4"
BANTIME_HOUR=$(echo "scale=2; $BANTIME_SEC/3600" | bc)
MESSAGE="🚨 Fail2ban alert
- Jail: $JAIL_NAME
- IP: $IP
- Attempts: $RETRY_COUNT
- Ban time: $BANTIME_HOUR hrs
- Host: $(hostname)"
curl -X POST "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=<YOUR_WEBHOOK_KEY>" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"markdown\",\"markdown\":{\"content\":\"$MESSAGE\"}}" >/dev/null 2>&1
exit $?Reference it in jail.local:
action = iptables-multiport[name=sshd, port="ssh", protocol=tcp]
/etc/fail2ban/action.d/wechat.sh[name=sshd]6.3 Slow‑attack detection
Traditional thresholds (5 attempts/10 min) miss low‑frequency attacks. Mitigations:
Lower maxretry and increase bantime so even occasional attempts lead to a long ban.
Use tools like pzkar to detect username‑enumeration behavior.
Employ sshguard or tighten MaxAuthTries and LoginGraceTime in sshd_config.
# Example hardening in /etc/ssh/sshd_config
MaxAuthTries 3
LoginGraceTime 30
ClientAliveInterval 300
ClientAliveCountMax 26.4 Prevent false positives: whitelist
Add trusted networks and IPs to ignoreip. For dynamic whitelisting, use ignorecommand to call a script that checks a trusted‑IP database.
[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 <office-ip> <jump-host-ip>
ignorecommand = /usr/local/bin/check_trusted_ip.sh <ip>6.5 Multi‑port protection (FTP, HTTP basic auth, Nginx)
Fail2ban can protect any service that logs authentication failures. Example jails:
[vsftpd]
enabled = true
filter = vsftpd
logpath = /var/log/vsftpd.log
bantime = 3600
maxretry = 5
action = iptables-multiport[name=vsftpd, port="ftp,ftp-data", protocol=tcp]
[nginx-http-auth]
enabled = false
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
bantime = 600
maxretry = 5
action = iptables-multiport[name=nginx-http-auth, port="http,https", protocol=tcp]7 Daily operations and troubleshooting
7.1 Common commands
# Show all jails
fail2ban-client status
# Show a specific jail (e.g., sshd)
fail2ban-client status sshd
# Manually ban or unban an IP
fail2ban-client set sshd banip 1.2.3.4
fail2ban-client set sshd unbanip 1.2.3.4
# Reload configuration without interrupting existing bans
fail2ban-client reload7.2 Fail2ban not working – checklist
Verify the Fail2ban service is running: systemctl status fail2ban and ps aux | grep fail2ban.
Test that the log format matches the failregex using fail2ban-regex. If no matches are found, adjust the regex or confirm the log file contains failure lines (e.g., grep -i "failed password" /var/log/auth.log).
Confirm iptables rules were created: iptables -L -n | grep -i f2b. An empty output means the ban action did not write rules.
Validate jail.local syntax (e.g.,
python3 -c "import configparser; c=configparser.ConfigParser(); c.read('/etc/fail2ban/jail.local'); print('OK')").
Ensure system time is correct; Fail2ban’s sliding window depends on accurate timestamps ( timedatectl status, enable NTP if needed).
7.3 Performance: large log files
If /var/log/auth.log or /var/log/secure grows to several gigabytes, scanning becomes slow. Use logrotate to rotate logs and notify Fail2ban.
/var/log/auth.log {
daily
rotate 14
compress
missingok
notifempty
create 0640 root adm
sharedscripts
postrotate
/usr/bin/fail2ban-client set logtarget /var/log/fail2ban.log 2>/dev/null || true
endscript
}7.4 Performance: too many iptables rules
When thousands of IPs are banned, iptables linear matching degrades. Switch to ipset for O(1) lookups.
# In jail.local use ipset action
action = iptables-ipset-proto-port[name=sshd]
# Manually create the set (Fail2ban will create it automatically)
ipset create f2b-sshd hash:ip timeout 3600
ipset list f2b-sshd8 Production‑ready configuration template
# /etc/fail2ban/jail.local
[DEFAULT]
# Ban time: 1 hour, progressive increase
bantime = 3600
bantime.increment = true
bantime.multipliers = 1 2 4 8 16 32 64
bantime.rndtime = 30
# Counting window: 5 minutes, 5 failures trigger a ban
findtime = 300
maxretry = 5
# Whitelist (adjust to your environment)
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 <jump-host-ip>
loglevel = INFO
logtarget = /var/log/fail2ban.log
[sshd]
enabled = true
filter = sshd
logpath = /var/log/auth.log # Debian/Ubuntu (use /var/log/secure for CentOS/RHEL)
bantime = 3600
maxretry = 5
findtime = 300
action = iptables-multiport[name=sshd, port="ssh", protocol=tcp]
/etc/fail2ban/action.d/wechat.sh[name=sshd]
[recidive]
enabled = true
filter = recidive
logpath = /var/log/fail2ban.log
banaction = iptables-allports
bantime = 604800 # 7 days
findtime = 86400 # 1 day window
maxretry = 39 Validation and monitoring
9.1 Functional verification
After configuration, test the ban flow either by performing a real failed SSH login from another host or by manually triggering a ban:
# Manual test
fail2ban-client set sshd banip 192.0.2.1
iptables -L f2b-sshd -n -v | grep 192.0.2.1 # should show a REJECT rule
# Unban and check logs
fail2ban-client set sshd unbanip 192.0.2.1
tail /var/log/fail2ban.log | grep 192.0.2.19.2 Monitoring and alerting
Export the number of currently banned IPs for Prometheus or other monitoring systems.
# Example for node_exporter textfile collector
BANNED_COUNT=$(fail2ban-client status sshd | grep "Currently banned" | awk '{print $NF}')
echo "fail2ban_sshd_banned_count $BANNED_COUNT" > /var/lib/node_exporter/textfile_collector/fail2ban.promAlternatively, use the third‑party fail2ban_exporter to expose detailed metrics.
10 Conclusion
Fail2ban is an effective tool for automatically blocking high‑frequency SSH brute‑force attempts, providing dynamic firewall rule management, progressive bans, and extensible alerting. However, it cannot stop low‑frequency or distributed attacks, nor replace strong authentication measures. For a robust SSH security posture, combine Fail2ban with key‑based login, two‑factor authentication, regular audit of login attempts, and broader hardening practices.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Raymond Ops
Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
