Set up fail2ban to automatically block SSH brute force attacks and protect your Linux server from unauthorized access attempts. Configure custom jails, firewall integration, and email notifications for comprehensive intrusion prevention.
Prerequisites
- Root or sudo access
- SSH service running
- Basic Linux command line knowledge
- Email service for notifications (optional)
What this solves
SSH brute force attacks are one of the most common threats to Linux servers, with attackers constantly scanning for weak passwords and misconfigurations. Fail2ban monitors log files for suspicious activity and automatically creates firewall rules to block repeat offenders, preventing unauthorized access attempts and reducing server load from malicious traffic. This tutorial shows you how to configure fail2ban with custom SSH protection rules, email notifications, and integration with modern Linux firewall backends like nftables and firewalld.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest security updates and dependencies.
sudo apt update && sudo apt upgrade -y
Install fail2ban and required dependencies
Install fail2ban along with email utilities for notifications and ensure the firewall backend is available.
sudo apt install -y fail2ban ufw sendmail iptables-persistent
Configure firewall backend
Enable and configure the appropriate firewall service for your distribution to work with fail2ban.
sudo ufw enable
sudo systemctl enable netfilter-persistent
Create fail2ban local configuration
Create a local configuration file to override default settings without modifying the original configuration files.
[DEFAULT]
Ban hosts for 1 hour (3600 seconds)
bantime = 3600
Find time window - 10 minutes
findtime = 600
Number of failures before ban
maxretry = 5
Backend for persistent bans
banaction = iptables-multiport
banaction_allports = iptables-allports
Email notifications
destemail = admin@example.com
sender = fail2ban@example.com
mta = sendmail
action = %(action_mwl)s
Whitelist your management IPs
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 7200
findtime = 300
Configure distribution-specific firewall backend
Set the appropriate firewall backend based on your Linux distribution for optimal compatibility.
Ubuntu/Debian specific backend
[DEFAULT]
banaction = ufw
banaction_allports = ufw
Create custom SSH filter for enhanced detection
Create a custom filter to catch additional SSH attack patterns beyond the default configuration.
[INCLUDES]
before = common.conf
[Definition]
failregex = ^%(__prefix_line)s(?:error: PAM: )?[aA]uthentication (?:failure|error|failed) for . from ( via \S+)?\s $
^%(__prefix_line)s(?:error: )?Received disconnect from : 3: .: Auth fail.$
^%(__prefix_line)sFailed (?:password|publickey) for . from (?: port \d )?(?: ssh\d*)?$
^%(__prefix_line)sROOT LOGIN REFUSED. FROM \s $
^%(__prefix_line)siI user . from \s $
^%(__prefix_line)sUser .+ from not allowed because not listed in AllowUsers\s*$
^%(__prefix_line)sUser .+ from not allowed because listed in DenyUsers\s*$
^%(__prefix_line)sUser .+ from not allowed because not in any group\s*$
^%(__prefix_line)srefused connect from \S+ \(\)\s*$
^%(__prefix_line)s(?:error: )?Received disconnect from : 14: No supported authentication methods available.*$
^%(__prefix_line)sSSH: Server;Ltype: Authname;Remote: -\d+;Name: [^;];(?:Key|Passwd): 0;Result: Failed.$
ignoreregex =
[Init]
journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd
Configure advanced SSH jail with custom settings
Replace the basic SSH configuration with advanced settings including the custom filter and escalating ban times.
[sshd-custom]
enabled = true
port = ssh
filter = sshd-custom
logpath = %(sshd_log)s
backend = %(sshd_backend)s
maxretry = 3
findtime = 300
bantime = 3600
Escalating ban times for repeat offenders
bantime.increment = true
bantime.factor = 2
bantime.formula = ban.Time (1<<(ban.Count if ban.Count<20 else 20)) banFactor
bantime.multipliers = 1 2 4 8 16 32 64 128 256 512
bantime.maxtime = 604800
Email alerts for this jail
action = %(action_mwl)s
%(action_cf_mwl)s[cftoken="YOUR_CF_TOKEN", cfuser="admin@example.com"]
Whitelist specific IPs (add your management IPs)
ignoreip = 127.0.0.1/8 ::1 203.0.113.0/24
Custom log path for different distributions
logpath = /var/log/auth.log
/var/log/secure
%(sshd_log)s
Configure email notification settings
Set up detailed email notifications to alert you when bans occur and provide useful information about the attacks.
[INCLUDES]
before = mail.conf
[Definition]
actionname = mail-custom
actionstart = printf %%b "Subject: [Fail2ban] %(name)s: started on %(hostname)s
Date: LC_ALL=C date -u +"%%a, %%d %%h %%Y %%T +0000"
From: %(sender)s
To: %(dest)s
Hi,
The jail %(name)s has been started successfully.
Regards,
Fail2Ban" | /usr/sbin/sendmail -f %(sender)s %(dest)s
actionstop = printf %%b "Subject: [Fail2ban] %(name)s: stopped on %(hostname)s
Date: LC_ALL=C date -u +"%%a, %%d %%h %%Y %%T +0000"
From: %(sender)s
To: %(dest)s
Hi,
The jail %(name)s has been stopped.
Regards,
Fail2Ban" | /usr/sbin/sendmail -f %(sender)s %(dest)s
actionban = printf %%b "Subject: [Fail2ban] %(name)s: banned %(ip)s from %(hostname)s
Date: LC_ALL=C date -u +"%%a, %%d %%h %%Y %%T +0000"
From: %(sender)s
To: %(dest)s
Hi,
The IP %(ip)s has just been banned by Fail2Ban after
%(failures)s attempts against %(name)s on %(hostname)s.
Ban duration: %(bantime)s seconds
Attack details:
%(matches)s
Regards,
Fail2Ban" | /usr/sbin/sendmail -f %(sender)s %(dest)s
actionunban = printf %%b "Subject: [Fail2ban] %(name)s: unbanned %(ip)s from %(hostname)s
Date: LC_ALL=C date -u +"%%a, %%d %%h %%Y %%T +0000"
From: %(sender)s
To: %(dest)s
Hi,
The IP %(ip)s has just been unbanned from %(name)s on %(hostname)s.
Regards,
Fail2Ban" | /usr/sbin/sendmail -f %(sender)s %(dest)s
[Init]
dest = admin@example.com
sender = fail2ban@example.com
name = default
Configure web service protection jails
Add additional jails to protect web services from common attacks like HTTP auth failures and exploit attempts.
[apache-auth]
enabled = true
filter = apache-auth
logpath = /var/log/apache/error.log
maxretry = 3
bantime = 3600
[apache-badbots]
enabled = true
filter = apache-badbots
logpath = /var/log/apache/access.log
maxretry = 2
bantime = 7200
[apache-noscript]
enabled = true
filter = apache-noscript
logpath = /var/log/apache/access.log
maxretry = 6
bantime = 3600
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 3
bantime = 3600
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime = 7200
[postfix-sasl]
enabled = true
filter = postfix-sasl
logpath = /var/log/mail.log
maxretry = 3
bantime = 3600
Set correct file permissions and ownership
Ensure fail2ban configuration files have proper permissions for security and functionality.
sudo chown -R root:root /etc/fail2ban/
sudo chmod 755 /etc/fail2ban/
sudo chmod 644 /etc/fail2ban/*.conf
sudo chmod 644 /etc/fail2ban/jail.d/*.conf
sudo chmod 644 /etc/fail2ban/filter.d/*.conf
sudo chmod 644 /etc/fail2ban/action.d/*.conf
Test configuration syntax
Validate your fail2ban configuration before starting the service to catch any syntax errors.
sudo fail2ban-client -t
sudo fail2ban-client --dp
Enable and start fail2ban service
Start fail2ban and enable it to run automatically at boot time.
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
sudo systemctl status fail2ban
Verify your setup
Check that fail2ban is running properly and monitoring your SSH service.
sudo fail2ban-client status
sudo fail2ban-client status sshd-custom
sudo fail2ban-client get sshd-custom logpath
sudo tail -f /var/log/fail2ban.log
Test the SSH protection by checking if fail2ban detects authentication failures:
sudo grep "Failed password" /var/log/auth.log | tail -5
sudo fail2ban-client status sshd-custom
View current banned IPs across all jails:
sudo fail2ban-client banned
Advanced configuration and monitoring
Configure persistent ban database
Set up a database to persist banned IPs across fail2ban restarts for better long-term protection.
# Persistent ban database
dbfile = /var/lib/fail2ban/fail2ban.sqlite3
dbpurgeage = 2592000
Create monitoring script
Create a script to monitor fail2ban status and generate reports of blocked attacks.
#!/bin/bash
echo "=== Fail2ban Status Report ==="
echo "Generated: $(date)"
echo ""
echo "Active Jails:"
fail2ban-client status | grep "Jail list" | sed 's/.*://'
echo ""
echo "Current Banned IPs:"
for jail in $(fail2ban-client status | grep "Jail list" | cut -d: -f2 | tr ',' '\n' | tr -d ' '); do
banned=$(fail2ban-client status $jail | grep "Banned IP list" | cut -d: -f2 | tr -d ' ')
if [ ! -z "$banned" ]; then
echo "$jail: $banned"
fi
done
echo ""
echo "Recent Ban Activity (last 24 hours):"
grep "$(date +'%Y-%m-%d')" /var/log/fail2ban.log | grep "Ban" | tail -10
echo ""
echo "Top Attacking IPs:"
grep "Ban" /var/log/fail2ban.log | awk '{print $NF}' | sort | uniq -c | sort -nr | head -10
Make monitoring script executable
Set proper permissions for the monitoring script and test its functionality.
sudo chmod 755 /usr/local/bin/fail2ban-report.sh
sudo /usr/local/bin/fail2ban-report.sh
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Fail2ban won't start | Configuration syntax error | sudo fail2ban-client -t to check syntax |
| SSH jail not working | Wrong log path | Check /var/log/auth.log exists or use /var/log/secure |
| IPs not getting banned | Firewall backend mismatch | Set correct banaction for your firewall (ufw/firewalld) |
| Email notifications not working | Sendmail not configured | sudo systemctl status sendmail and configure SMTP |
| Own IP got banned | Not in whitelist | Add IP to ignoreip and sudo fail2ban-client unban YOUR_IP |
| Bans not persisting | No database configured | Enable dbfile in jail.local configuration |
| High false positives | Too low maxretry | Increase maxretry value and adjust findtime window |
Next steps
- Configure Linux system firewall with nftables and security hardening for advanced firewall rules
- Configure ClamAV antivirus scanning with automated threat detection and email alerts for additional malware protection
- Configure SSH key authentication and security hardening to reduce brute force attack surface
- Set up fail2ban with Cloudflare API integration for CDN-level blocking
- Configure intrusion detection with OSSEC and fail2ban integration for comprehensive security monitoring
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Production fail2ban SSH protection installer
# Supports Ubuntu, Debian, AlmaLinux, Rocky Linux, CentOS, RHEL, Fedora
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Default values
EMAIL="${1:-admin@example.com}"
WHITELIST_IP="${2:-}"
usage() {
echo "Usage: $0 [email] [whitelist_ip]"
echo "Example: $0 admin@domain.com 203.0.113.100"
exit 1
}
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1"
exit 1
}
cleanup() {
if [[ $? -ne 0 ]]; then
error "Installation failed. Check logs above for details."
fi
}
trap cleanup ERR
# Check prerequisites
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root or with sudo"
fi
if [[ "$EMAIL" == *"example.com" ]]; then
warn "Using example email address. Consider providing a real email."
fi
# Auto-detect distribution
if [[ ! -f /etc/os-release ]]; then
error "Cannot detect distribution. /etc/os-release not found."
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
FIREWALL_CMD="ufw"
LOG_PATH="/var/log/auth.log"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
FIREWALL_CMD="firewalld"
LOG_PATH="/var/log/secure"
;;
fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
FIREWALL_CMD="firewalld"
LOG_PATH="/var/log/secure"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
FIREWALL_CMD="firewalld"
LOG_PATH="/var/log/secure"
;;
*)
error "Unsupported distribution: $ID"
;;
esac
log "Detected distribution: $PRETTY_NAME"
echo "[1/8] Updating system packages..."
$PKG_UPDATE
echo "[2/8] Installing fail2ban and dependencies..."
if [[ "$PKG_MGR" == "apt" ]]; then
$PKG_INSTALL fail2ban ufw sendmail iptables-persistent
else
$PKG_INSTALL fail2ban firewalld sendmail iptables-services
fi
echo "[3/8] Configuring firewall backend..."
if [[ "$FIREWALL_CMD" == "ufw" ]]; then
ufw --force enable
systemctl enable netfilter-persistent
else
systemctl enable --now firewalld
firewall-cmd --permanent --add-service=ssh
firewall-cmd --reload
fi
echo "[4/8] Creating fail2ban local configuration..."
WHITELIST_CONFIG="127.0.0.1/8 ::1"
if [[ -n "$WHITELIST_IP" ]]; then
WHITELIST_CONFIG="$WHITELIST_CONFIG $WHITELIST_IP"
fi
cat > /etc/fail2ban/jail.local << EOF
[DEFAULT]
# Ban hosts for 1 hour (3600 seconds)
bantime = 3600
# Find time window - 10 minutes
findtime = 600
# Number of failures before ban
maxretry = 5
# Email notifications
destemail = $EMAIL
sender = fail2ban@$(hostname -d 2>/dev/null || echo localhost)
mta = sendmail
action = %(action_mwl)s
# Whitelist your management IPs
ignoreip = $WHITELIST_CONFIG
[sshd]
enabled = true
port = ssh
filter = sshd-custom
logpath = $LOG_PATH
maxretry = 3
bantime = 7200
findtime = 300
EOF
echo "[5/8] Setting distribution-specific firewall backend..."
if [[ "$FIREWALL_CMD" == "ufw" ]]; then
cat >> /etc/fail2ban/jail.local << EOF
# Ubuntu/Debian specific backend
banaction = ufw
banaction_allports = ufw
EOF
else
cat >> /etc/fail2ban/jail.local << EOF
# RHEL/Fedora specific backend
banaction = firewallcmd-ipset
banaction_allports = firewallcmd-allports
EOF
fi
chmod 644 /etc/fail2ban/jail.local
echo "[6/8] Creating custom SSH filter..."
cat > /etc/fail2ban/filter.d/sshd-custom.conf << 'EOF'
[INCLUDES]
before = common.conf
[Definition]
failregex = ^%(__prefix_line)s(?:error: PAM: )?[aA]uthentication (?:failure|error|failed) for .* from <HOST>( via \S+)?\s*$
^%(__prefix_line)s(?:error: )?Received disconnect from <HOST>: 3: .*: Auth fail.*$
^%(__prefix_line)sFailed (?:password|publickey) for .* from <HOST>(?: port \d+)?(?: ssh\d*)?$
^%(__prefix_line)sROOT LOGIN REFUSED .* FROM <HOST>\s*$
^%(__prefix_line)s[iI]nvalid user .* from <HOST>\s*$
^%(__prefix_line)sUser .+ from <HOST> not allowed because not listed in AllowUsers\s*$
^%(__prefix_line)sUser .+ from <HOST> not allowed because listed in DenyUsers\s*$
^%(__prefix_line)sUser .+ from <HOST> not allowed because not in any group\s*$
^%(__prefix_line)srefused connect from \S+ \(<HOST>\)\s*$
^%(__prefix_line)s(?:error: )?Received disconnect from <HOST>: 14: No supported authentication methods available.*$
ignoreregex =
[Init]
journalmatch = _SYSTEMD_UNIT=sshd.service + _COMM=sshd
EOF
chmod 644 /etc/fail2ban/filter.d/sshd-custom.conf
echo "[7/8] Starting and enabling fail2ban service..."
systemctl enable fail2ban
systemctl restart fail2ban
# Wait for service to start
sleep 3
echo "[8/8] Verifying installation..."
if ! systemctl is-active --quiet fail2ban; then
error "fail2ban service is not running"
fi
if ! fail2ban-client status sshd >/dev/null 2>&1; then
error "SSH jail is not active"
fi
log "Installation completed successfully!"
log "fail2ban status:"
fail2ban-client status
log "SSH jail status:"
fail2ban-client status sshd
log "Configuration files:"
log " - Main config: /etc/fail2ban/jail.local"
log " - Custom filter: /etc/fail2ban/filter.d/sshd-custom.conf"
log " - Log monitoring: $LOG_PATH"
log "Email notifications will be sent to: $EMAIL"
if [[ -n "$WHITELIST_IP" ]]; then
log "Whitelisted IP: $WHITELIST_IP"
fi
warn "Test SSH access from another terminal before closing this session!"
Review the script before running. Execute with: bash install.sh