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.

Raymond Ops
Raymond Ops
Raymond Ops
Defend Against SSH Brute‑Force Attacks with Fail2ban: Real‑World Auto‑Blocking Guide

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 status

Configuration 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 settings

3.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: sshd

If 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 rsyslog

4 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 -20

4.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 ssh2

If 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 -v

The 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 fail2ban

5.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 REJECT

6 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 bans

6.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 2

6.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 reload

7.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-sshd

8 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 = 3

9 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.1

9.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.prom

Alternatively, 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

iptablesLinux securitySSHbrute-force protectionFail2ban
Raymond Ops
Written by

Raymond Ops

Linux ops automation, cloud-native, Kubernetes, SRE, DevOps, Python, Golang and related tech discussions.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.