Configure Fail2ban to automatically add malicious IPs to Cloudflare's firewall rules for enhanced protection. This tutorial covers installation, custom filters, API integration, and monitoring for comprehensive security automation across your infrastructure.
Prerequisites
- Root or sudo access
- Active Cloudflare account with API access
- Domain configured with Cloudflare DNS
- Python 3.6 or later
What this solves
Fail2ban blocks malicious IPs by monitoring log files and creating temporary firewall rules. When integrated with Cloudflare's API, blocked IPs are automatically added to your Cloudflare firewall, protecting all services behind your domain. This creates a distributed security layer that scales across multiple servers and services.
Step-by-step installation
Update system packages
Start by updating your package manager to ensure you get the latest versions of security tools.
sudo apt update && sudo apt upgrade -y
Install Fail2ban and dependencies
Install Fail2ban along with Python3 and pip for the Cloudflare API integration script.
sudo apt install -y fail2ban python3 python3-pip curl jq
pip3 install --user requests python-cloudflare
Create Cloudflare API credentials
Generate API credentials from your Cloudflare dashboard for zone and firewall management.
sudo mkdir -p /etc/fail2ban/scripts
sudo touch /etc/fail2ban/cloudflare.conf
sudo chmod 600 /etc/fail2ban/cloudflare.conf
Add your Cloudflare credentials to the configuration file:
[cloudflare]
api_token = your_api_token_here
zone_id = your_zone_id_here
email = your_cloudflare_email@example.com
api_key = your_global_api_key_here
Create Cloudflare integration script
This Python script handles adding and removing IPs from Cloudflare firewall rules.
#!/usr/bin/env python3
import sys
import requests
import configparser
import json
import time
config = configparser.ConfigParser()
config.read('/etc/fail2ban/cloudflare.conf')
API_TOKEN = config.get('cloudflare', 'api_token')
ZONE_ID = config.get('cloudflare', 'zone_id')
BASE_URL = 'https://api.cloudflare.com/client/v4'
headers = {
'Authorization': f'Bearer {API_TOKEN}',
'Content-Type': 'application/json'
}
def ban_ip(ip_address):
"""Add IP to Cloudflare firewall rules"""
rule_data = {
'mode': 'block',
'configuration': {
'target': 'ip',
'value': ip_address
},
'notes': f'Fail2ban auto-ban: {ip_address} at {time.strftime("%Y-%m-%d %H:%M:%S")}'
}
response = requests.post(
f'{BASE_URL}/zones/{ZONE_ID}/firewall/access_rules/rules',
headers=headers,
json=rule_data
)
if response.status_code == 200:
print(f'Successfully banned {ip_address} in Cloudflare')
return True
else:
print(f'Failed to ban {ip_address}: {response.text}')
return False
def unban_ip(ip_address):
"""Remove IP from Cloudflare firewall rules"""
# First, find the rule ID
response = requests.get(
f'{BASE_URL}/zones/{ZONE_ID}/firewall/access_rules/rules',
headers=headers,
params={'configuration.target': 'ip', 'configuration.value': ip_address}
)
if response.status_code == 200:
rules = response.json().get('result', [])
for rule in rules:
rule_id = rule['id']
delete_response = requests.delete(
f'{BASE_URL}/zones/{ZONE_ID}/firewall/access_rules/rules/{rule_id}',
headers=headers
)
if delete_response.status_code == 200:
print(f'Successfully unbanned {ip_address} from Cloudflare')
return True
print(f'Failed to unban {ip_address} or rule not found')
return False
if __name__ == '__main__':
if len(sys.argv) != 3:
print('Usage: cloudflare-ban.py ')
sys.exit(1)
action = sys.argv[1]
ip_address = sys.argv[2]
if action == 'ban':
ban_ip(ip_address)
elif action == 'unban':
unban_ip(ip_address)
else:
print('Invalid action. Use "ban" or "unban"')
sys.exit(1)
Make the script executable:
sudo chmod +x /etc/fail2ban/scripts/cloudflare-ban.py
Configure Fail2ban main settings
Create the main Fail2ban configuration with enhanced security settings.
[DEFAULT]
Ban settings
bantime = 3600
findtime = 600
maxretry = 3
banaction = iptables-multiport
banaction_allports = iptables-allports
Email notifications
destemail = admin@example.com
sendername = Fail2ban-Server
mta = sendmail
Cloudflare integration
action = %(action_mwl)s
cloudflare-ban
Ignore local networks
ignoreip = 127.0.0.1/8 ::1 10.0.0.0/8 192.168.0.0/16 172.16.0.0/12
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
bantime = 7200
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 2
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
maxretry = 10
bantime = 3600
[apache-auth]
enabled = false
filter = apache-auth
logpath = /var/log/apache2/error.log
maxretry = 2
[apache-badbots]
enabled = false
filter = apache-badbots
logpath = /var/log/apache2/access.log
maxretry = 1
bantime = 86400
Create Cloudflare action configuration
Define the Cloudflare ban action that Fail2ban will execute.
[Definition]
Action to ban IP addresses in Cloudflare
actionstart =
actionstop =
actioncheck =
actionban = /etc/fail2ban/scripts/cloudflare-ban.py ban
actionunban = /etc/fail2ban/scripts/cloudflare-ban.py unban
[Init]
Default values
name = cloudflare-ban
Create custom filters for enhanced detection
Add custom filters to detect additional attack patterns beyond the default ones.
[Definition]
failregex = ^\s\[error\].limiting requests, excess:. by zone.client:
ignoreregex =
[Definition]
failregex = ^\s\[error\]. user .* password mismatch, client:
^\s\[error\]. user . was not found in ., client:
^\s\[error\]. access forbidden by rule, client:
ignoreregex =
[Definition]
Custom filter for applications behind Cloudflare
Looks for real IP in CF-Connecting-IP header
failregex = ^.\[CF-Connecting-IP: \]. login failed
^.\[CF-Connecting-IP: \]. authentication failed
^.\[CF-Connecting-IP: \]. invalid password
ignoreregex =
Configure log monitoring and alerting
Set up enhanced logging and email notifications for security events.
[DEFAULT]
Enhanced email notifications
action = %(action_mwl)s
cloudflare-ban
Custom notification action
action_mwl = %(banaction)s[name=%(__name__)s, bantime="%(bantime)s", port="%(port)s", protocol="%(protocol)s", chain="%(chain)s"]
%(mta)s-whois-lines[name=%(__name__)s, sender="%(sender)s", dest="%(destemail)s", logpath=%(logpath)s, chain="%(chain)s"]
Slack webhook notification (optional)
[slack-notify]
enabled = false
filter =
logpath = /var/log/fail2ban.log
maxretry = 1
bantime = 60
findtime = 60
action = slack[webhook="https://hooks.slack.com/your/webhook/url"]
Enable and start Fail2ban
Start the Fail2ban service and enable it to start automatically on boot.
sudo systemctl enable --now fail2ban
sudo systemctl status fail2ban
Test the Cloudflare integration
Verify that the Cloudflare API integration works correctly.
# Test the ban script
sudo /etc/fail2ban/scripts/cloudflare-ban.py ban 203.0.113.10
Check if the IP was added to Cloudflare
curl -X GET "https://api.cloudflare.com/client/v4/zones/YOUR_ZONE_ID/firewall/access_rules/rules" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" | jq '.result[] | select(.configuration.value=="203.0.113.10")'
Test the unban script
sudo /etc/fail2ban/scripts/cloudflare-ban.py unban 203.0.113.10
Advanced configuration options
Configure geo-blocking integration
Add geographical restrictions to complement IP-based blocking.
[Definition]
actionstart =
actionstop =
actioncheck =
actionban = /etc/fail2ban/scripts/cloudflare-geo.py ban
actionunban =
[Init]
name = cloudflare-geo
Set up threat intelligence integration
Integrate with threat intelligence feeds for proactive blocking.
#!/usr/bin/env python3
import requests
import json
from datetime import datetime, timedelta
def check_threat_intel(ip_address):
"""Check IP against threat intelligence feeds"""
# AbuseIPDB integration (requires API key)
abuseipdb_key = "your_abuseipdb_api_key"
headers = {
'Key': abuseipdb_key,
'Accept': 'application/json'
}
params = {
'ipAddress': ip_address,
'maxAgeInDays': 90,
'verbose': ''
}
try:
response = requests.get(
'https://api.abuseipdb.com/api/v2/check',
headers=headers,
params=params
)
if response.status_code == 200:
data = response.json()
confidence = data.get('data', {}).get('abuseConfidencePercentage', 0)
return confidence > 50 # Ban if confidence > 50%
except Exception as e:
print(f"Threat intel check failed: {e}")
return False
if __name__ == '__main__':
import sys
if len(sys.argv) > 1:
ip = sys.argv[1]
if check_threat_intel(ip):
print(f"IP {ip} flagged by threat intelligence")
sys.exit(0)
else:
print(f"IP {ip} not flagged")
sys.exit(1)
sudo chmod +x /etc/fail2ban/scripts/threat-intel.py
Configure monitoring and metrics
Set up monitoring integration for security event tracking. This complements existing monitoring setups like Prometheus and Grafana monitoring.
#!/usr/bin/env python3
import json
import time
from datetime import datetime
import subprocess
def send_metrics(action, ip, jail_name):
"""Send metrics to monitoring system"""
metric_data = {
'timestamp': datetime.utcnow().isoformat(),
'action': action,
'ip': ip,
'jail': jail_name,
'hostname': subprocess.getoutput('hostname')
}
# Write to log file for collection by log aggregation
with open('/var/log/fail2ban-metrics.json', 'a') as f:
f.write(json.dumps(metric_data) + '\n')
# Optional: Send to metrics endpoint
# requests.post('http://your-metrics-endpoint', json=metric_data)
if __name__ == '__main__':
import sys
if len(sys.argv) >= 4:
send_metrics(sys.argv[1], sys.argv[2], sys.argv[3])
sudo chmod +x /etc/fail2ban/scripts/metrics.py
Verify your setup
Check that Fail2ban is running and monitoring your services correctly.
# Check Fail2ban status
sudo systemctl status fail2ban
List active jails
sudo fail2ban-client status
Check specific jail status
sudo fail2ban-client status sshd
View recent bans
sudo fail2ban-client get sshd bantime
sudo fail2ban-client get sshd banip
Test log parsing
sudo fail2ban-regex /var/log/auth.log /etc/fail2ban/filter.d/sshd.conf
Check Cloudflare API connectivity
curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json"
Monitor real-time activity
sudo tail -f /var/log/fail2ban.log
Performance optimization
Fine-tune Fail2ban for better performance on busy servers.
# Add to [DEFAULT] section for high-traffic servers
[DEFAULT]
Reduce log polling frequency for better performance
backend = systemd
Optimize for high log volumes
usedns = no
logencoding = auto
Adjust timeouts
bantime.increment = true
bantime.rndtime = 300
bantime.maxtime = 86400
bantime.factor = 2
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Fail2ban won't start | Configuration syntax error | sudo fail2ban-client -t to test config |
| Cloudflare API calls fail | Invalid API token or permissions | Verify token has Zone:Read and Zone:Edit permissions |
| No bans triggered | Log file permissions or path | Check ls -la /var/log/auth.log and Fail2ban user permissions |
| Email notifications not working | MTA not configured | Install and configure postfix: sudo apt install postfix |
| High CPU usage | Too frequent log polling | Increase findtime and use backend = systemd |
| IPs not unbanning automatically | Cloudflare script errors | Check /var/log/fail2ban.log for script execution errors |
| False positive bans | Aggressive filters | Increase maxretry values and add IPs to ignoreip |
Next steps
- Configure NGINX rate limiting and DDoS protection for additional web server security
- Set up ModSecurity with machine learning for advanced threat detection
- Configure centralized logging for better log management across multiple servers
- Set up distributed Fail2ban monitoring dashboard for multi-server oversight
- Implement ML-based threat detection with Fail2ban for intelligent blocking
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Script variables
SCRIPT_NAME="fail2ban-cloudflare-setup"
TOTAL_STEPS=8
# Function to print colored output
print_status() {
echo -e "${GREEN}[INFO]${NC} $1"
}
print_warning() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
print_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
print_step() {
echo -e "${BLUE}[$1/$TOTAL_STEPS]${NC} $2"
}
# Usage function
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " -t, --token TOKEN Cloudflare API Token"
echo " -z, --zone ZONE_ID Cloudflare Zone ID"
echo " -e, --email EMAIL Cloudflare email"
echo " -k, --key API_KEY Cloudflare Global API Key"
echo " -h, --help Show this help message"
echo ""
echo "Example:"
echo " $0 -t your_api_token -z your_zone_id -e email@domain.com -k global_api_key"
exit 1
}
# Cleanup function for rollback
cleanup() {
print_error "Installation failed. Cleaning up..."
systemctl stop fail2ban 2>/dev/null || true
rm -f /etc/fail2ban/scripts/cloudflare-ban.py 2>/dev/null || true
rm -f /etc/fail2ban/cloudflare.conf 2>/dev/null || true
rm -f /etc/fail2ban/jail.d/cloudflare.conf 2>/dev/null || true
print_error "Cleanup completed. Please check the error above."
}
trap cleanup ERR
# Check if running as root
if [[ $EUID -ne 0 ]]; then
print_error "This script must be run as root or with sudo"
exit 1
fi
# Parse command line arguments
API_TOKEN=""
ZONE_ID=""
CF_EMAIL=""
API_KEY=""
while [[ $# -gt 0 ]]; do
case $1 in
-t|--token)
API_TOKEN="$2"
shift 2
;;
-z|--zone)
ZONE_ID="$2"
shift 2
;;
-e|--email)
CF_EMAIL="$2"
shift 2
;;
-k|--key)
API_KEY="$2"
shift 2
;;
-h|--help)
usage
;;
*)
print_error "Unknown option: $1"
usage
;;
esac
done
# Validate required parameters
if [[ -z "$API_TOKEN" || -z "$ZONE_ID" || -z "$CF_EMAIL" || -z "$API_KEY" ]]; then
print_error "Missing required parameters"
usage
fi
# Detect distribution
print_step "1" "Detecting operating system..."
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update && apt upgrade -y"
PYTHON_PKG="python3-pip"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf update -y"
PYTHON_PKG="python3-pip"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum update -y"
PYTHON_PKG="python3-pip"
;;
*)
print_error "Unsupported distribution: $ID"
exit 1
;;
esac
print_status "Detected: $PRETTY_NAME"
else
print_error "Cannot detect operating system"
exit 1
fi
# Update system packages
print_step "2" "Updating system packages..."
$PKG_UPDATE
# Install Fail2ban and dependencies
print_step "3" "Installing Fail2ban and dependencies..."
$PKG_INSTALL fail2ban python3 $PYTHON_PKG curl jq
# Install Python packages
print_status "Installing Python dependencies..."
pip3 install --user requests python-cloudflare
# Create directories and set permissions
print_step "4" "Creating configuration directories..."
mkdir -p /etc/fail2ban/scripts
mkdir -p /etc/fail2ban/jail.d
# Create Cloudflare API credentials file
print_step "5" "Creating Cloudflare API credentials..."
cat > /etc/fail2ban/cloudflare.conf << EOF
[cloudflare]
api_token = $API_TOKEN
zone_id = $ZONE_ID
email = $CF_EMAIL
api_key = $API_KEY
EOF
chmod 600 /etc/fail2ban/cloudflare.conf
chown root:root /etc/fail2ban/cloudflare.conf
# Create Cloudflare integration script
print_step "6" "Creating Cloudflare integration script..."
cat > /etc/fail2ban/scripts/cloudflare-ban.py << 'EOF'
#!/usr/bin/env python3
import sys
import requests
import configparser
import json
import time
config = configparser.ConfigParser()
config.read('/etc/fail2ban/cloudflare.conf')
API_TOKEN = config.get('cloudflare', 'api_token')
ZONE_ID = config.get('cloudflare', 'zone_id')
BASE_URL = 'https://api.cloudflare.com/client/v4'
headers = {
'Authorization': f'Bearer {API_TOKEN}',
'Content-Type': 'application/json'
}
def ban_ip(ip_address):
"""Add IP to Cloudflare firewall rules"""
rule_data = {
'mode': 'block',
'configuration': {
'target': 'ip',
'value': ip_address
},
'notes': f'Fail2ban auto-ban: {ip_address} at {time.strftime("%Y-%m-%d %H:%M:%S")}'
}
response = requests.post(
f'{BASE_URL}/zones/{ZONE_ID}/firewall/access_rules/rules',
headers=headers,
json=rule_data
)
if response.status_code == 200:
print(f'Successfully banned {ip_address} in Cloudflare')
return True
else:
print(f'Failed to ban {ip_address}: {response.text}')
return False
def unban_ip(ip_address):
"""Remove IP from Cloudflare firewall rules"""
response = requests.get(
f'{BASE_URL}/zones/{ZONE_ID}/firewall/access_rules/rules',
headers=headers,
params={'configuration.target': 'ip', 'configuration.value': ip_address}
)
if response.status_code == 200:
rules = response.json().get('result', [])
for rule in rules:
rule_id = rule['id']
delete_response = requests.delete(
f'{BASE_URL}/zones/{ZONE_ID}/firewall/access_rules/rules/{rule_id}',
headers=headers
)
if delete_response.status_code == 200:
print(f'Successfully unbanned {ip_address} from Cloudflare')
return True
print(f'Failed to unban {ip_address} or rule not found')
return False
if __name__ == '__main__':
if len(sys.argv) != 3:
print('Usage: cloudflare-ban.py <ban|unban> <ip_address>')
sys.exit(1)
action = sys.argv[1]
ip_address = sys.argv[2]
if action == 'ban':
ban_ip(ip_address)
elif action == 'unban':
unban_ip(ip_address)
else:
print('Invalid action. Use "ban" or "unban"')
sys.exit(1)
EOF
chmod 755 /etc/fail2ban/scripts/cloudflare-ban.py
chown root:root /etc/fail2ban/scripts/cloudflare-ban.py
# Create Fail2ban jail configuration
print_step "7" "Configuring Fail2ban jails..."
cat > /etc/fail2ban/jail.d/cloudflare.conf << EOF
[DEFAULT]
# Cloudflare action
action_cf = /etc/fail2ban/scripts/cloudflare-ban.py ban <ip>
action_cf_unban = /etc/fail2ban/scripts/cloudflare-ban.py unban <ip>
[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
bantime = 3600
findtime = 600
action = %(action_)s
%(action_cf)s
[nginx-http-auth]
enabled = false
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 3
bantime = 3600
findtime = 600
action = %(action_)s
%(action_cf)s
[apache-auth]
enabled = false
port = http,https
filter = apache-auth
logpath = /var/log/apache*/*error.log
maxretry = 3
bantime = 3600
findtime = 600
action = %(action_)s
%(action_cf)s
EOF
chmod 644 /etc/fail2ban/jail.d/cloudflare.conf
chown root:root /etc/fail2ban/jail.d/cloudflare.conf
# Enable and start services
print_step "8" "Starting and enabling Fail2ban service..."
systemctl enable fail2ban
systemctl restart fail2ban
# Verification
print_status "Verifying installation..."
sleep 5
if systemctl is-active --quiet fail2ban; then
print_status "✓ Fail2ban service is running"
else
print_error "✗ Fail2ban service is not running"
exit 1
fi
if python3 /etc/fail2ban/scripts/cloudflare-ban.py 2>/dev/null; then
print_status "✓ Cloudflare integration script is executable"
else
print_warning "⚠ Cloudflare script may have issues (this is normal if no arguments provided)"
fi
if fail2ban-client status &>/dev/null; then
print_status "✓ Fail2ban client is responding"
print_status "Active jails: $(fail2ban-client status | grep 'Jail list' | cut -d: -f2 | xargs)"
else
print_error "✗ Fail2ban client is not responding"
exit 1
fi
print_status ""
print_status "Installation completed successfully!"
print_status ""
print_status "Next steps:"
print_status "1. Monitor logs: tail -f /var/log/fail2ban.log"
print_status "2. Check status: fail2ban-client status"
print_status "3. Test jail: fail2ban-client status sshd"
print_status "4. Enable additional jails in /etc/fail2ban/jail.d/cloudflare.conf as needed"
print_status ""
print_warning "Remember to test the Cloudflare integration and monitor the logs!"
Review the script before running. Execute with: bash install.sh