Most tutorials stop at installing Fail2Ban with the default SSH jail. But a production Hong Kong VPS faces brute-force attacks on WordPress logins, Nginx rate limit violations, MySQL authentication, and web application endpoints — none of which the default configuration protects. This guide configures Fail2Ban with custom jails for every major attack surface, turning your VPS into a server that automatically blocks attackers in real time.
How Fail2Ban Works
Fail2Ban monitors log files for patterns matching failed authentication or abuse. When a source IP triggers a pattern more than maxretry times within findtime seconds, Fail2Ban adds an iptables rule banning that IP for bantime seconds. When bantime expires, the rule is removed.
Step 1: Install and Verify
apt install fail2ban -y
systemctl enable --now fail2ban
fail2ban-client status # Should show "Number of jail: 0" initiallyAlways create overrides in /etc/fail2ban/jail.local — never edit jail.conf directly (it gets overwritten on updates).
cat > /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
# Global defaults — apply to all jails unless overridden
bantime = 3600 # 1 hour ban
findtime = 600 # Look back 10 minutes
maxretry = 5 # Ban after 5 failures
backend = systemd # Use systemd journal (more reliable than file watching)
# Send email alerts (optional — configure postfix or nullmailer first)
# destemail = admin@yourdomain.com
# action = %(action_mwl)s # Ban + whois + log lines email
# Never ban these IPs (your own office IPs, monitoring services)
ignoreip = 127.0.0.1/8 ::1 YOUR_OFFICE_IP
EOFStep 2: SSH Jail (Enhanced)
<code">cat >> /etc/fail2ban/jail.local << 'EOF' [sshd] enabled = true port = 22222 # Your custom SSH port (from security hardening guide) filter = sshd backend = systemd maxretry = 3 # Stricter — SSH should have very low tolerance bantime = 86400 # 24 hour ban for SSH brute force findtime = 300 EOF
Step 3: Nginx Jails
Nginx 400/403/404 Flood
cat > /etc/fail2ban/filter.d/nginx-4xx.conf << 'EOF'
[Definition]
failregex = ^ -.*"(GET|POST|HEAD).*" (400|401|403|404|405|444) .*$
ignoreregex = ^ -.*"GET /(favicon\.ico|robots\.txt) HTTP.*" (403|404)
EOFNginx Authentication Failures
<code">cat > /etc/fail2ban/filter.d/nginx-http-auth.conf << 'EOF' [Definition] failregex = ^ \[error\] \d+#\d+: \*\d+ user "\S+":? (password mismatch|was not found in ".*"), client: ignoreregex = EOF
Add Nginx Jails to jail.local
<code">cat >> /etc/fail2ban/jail.local << 'EOF' [nginx-4xx] enabled = true filter = nginx-4xx logpath = /var/log/nginx/access.log maxretry = 20 findtime = 60 bantime = 3600 [nginx-http-auth] enabled = true filter = nginx-http-auth logpath = /var/log/nginx/error.log maxretry = 3 bantime = 3600 EOF
Step 4: WordPress Login Protection
<code">cat > /etc/fail2ban/filter.d/wordpress.conf << 'EOF'
[Definition]
# Catch failed wp-login.php attempts
failregex = ^ -.*"POST .*wp-login\.php.*" (200|403) .*$
^ -.*"POST .*xmlrpc\.php.*" (200|403) .*$
ignoreregex =
EOF
cat >> /etc/fail2ban/jail.local << 'EOF'
[wordpress]
enabled = true
filter = wordpress
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 86400 # 24 hours — login brutes deserve long bans
EOFStep 5: MySQL / MariaDB Protection
cat > /etc/fail2ban/filter.d/mysqld-auth.conf << 'EOF'
[Definition]
failregex = ^\d{6}\s+\d{1,2}:\d{2}:\d{2}\s+\[\w+\]\s+Access denied for user '[^']+'@''
^.*Access denied for user '.*'@'' \(using password: (YES|NO)\)$
ignoreregex =
EOF
cat >> /etc/fail2ban/jail.local << 'EOF'
[mysqld-auth]
enabled = true
filter = mysqld-auth
logpath = /var/log/mysql/error.log
maxretry = 5
bantime = 3600
EOFStep 6: Custom Application API Rate Limiting
<code">cat > /etc/fail2ban/filter.d/api-abuse.conf << 'EOF' [Definition] # Ban IPs that hit 429 rate limit responses repeatedly failregex = ^ -.*"(GET|POST).*" 429 .*$ ignoreregex = EOF cat >> /etc/fail2ban/jail.local << 'EOF' [api-abuse] enabled = true filter = api-abuse logpath = /var/log/nginx/access.log maxretry = 10 findtime = 60 bantime = 7200 # 2 hour ban for rate limit abusers EOF
Step 7: Recurrence-Based Escalating Bans
Standard Fail2Ban bans expire. Persistent attackers simply wait and try again. Configure escalating ban durations for repeat offenders using a recidive jail:
<code">cat >> /etc/fail2ban/jail.local << 'EOF' [recidive] enabled = true filter = recidive logpath = /var/log/fail2ban.log action = iptables-allports[name=recidive] maxretry = 3 # Ban again after 3 bans within 1 day findtime = 86400 # Look back 24 hours bantime = 604800 # 1 week ban for persistent attackers EOF
The recidive jail reads Fail2Ban’s own log — if an IP gets banned 3 times in 24 hours across any jail, it receives a 7-day block on all ports. This eliminates persistent attackers that cycle through attempts during ban windows.
Step 8: Reload and Verify
<code">fail2ban-client reload fail2ban-client status # Should show all configured jails: # Status # |- Number of jail: 7 # `- Jail list: sshd, nginx-4xx, nginx-http-auth, wordpress, mysqld-auth, api-abuse, recidive # Check specific jail status fail2ban-client status wordpress # Shows currently banned IPs and recent activity # Manually ban an IP (for testing) fail2ban-client set wordpress banip 1.2.3.4 # Manually unban fail2ban-client set wordpress unbanip 1.2.3.4
Step 9: Geographic Blocking for High-Risk Regions (Optional)
If your application has no legitimate users from certain high-attack-volume regions, geographic IP blocking reduces noise before Fail2Ban even needs to act:
<code">apt install -y ipset
# Download country IP ranges (example: block known high-attack regions)
# This is a policy decision — only block where you have zero legitimate users
# Example: block a specific country's IP ranges
# Better approach: use Nginx geo module to rate-limit suspicious regions
# rather than outright blocking (less aggressive, fewer false positives)
# In nginx.conf http block:
geo $suspicious_region {
default 0;
# Add specific CIDR ranges for high-risk regions here
# 1.2.3.0/24 1;
}Geographic blocking is a blunt instrument — only use it if you have clear data showing attacks from regions where you have no legitimate users. CN2 GIA routing means legitimate Chinese users connect to your Hong Kong VPS; blocking Chinese IPs would block your actual users.
Monitoring Fail2Ban Activity
<code"># Real-time ban monitoring
tail -f /var/log/fail2ban.log | grep "Ban"
# Today's bans by jail
grep "$(date +%Y-%m-%d)" /var/log/fail2ban.log | grep "Ban" | \
awk '{print $6}' | sort | uniq -c | sort -rn
# All currently banned IPs across all jails
fail2ban-client status | grep "Jail list" | \
sed 's/.*Jail list://;s/ //g' | tr ',' '\n' | \
while read jail; do
BANNED=$(fail2ban-client status "$jail" | grep "Banned IP list" | cut -d: -f2)
[ -n "$BANNED" ] && echo "$jail: $BANNED"
done
# Top attacking IPs over the past 7 days
grep "Ban" /var/log/fail2ban.log | grep -oP '\d+\.\d+\.\d+\.\d+' | \
sort | uniq -c | sort -rn | head 20Conclusion
A properly configured Fail2Ban on your Hong Kong VPS with jails for SSH, Nginx, WordPress, MySQL, and API abuse covers every major automated attack vector. The recidive jail eliminates persistent attackers that time their retries around standard ban windows. Combined with the security hardening guide (SSH keys, UFW firewall, Sysctl hardening), Fail2Ban completes your VPS’s automated defence layer — blocking tens of thousands of attack attempts per day without manual intervention.
Secure your server: Browse Server.HK Hong Kong VPS plans — all plans include KVM isolation and DDoS protection at the network level; Fail2Ban adds application-level automated defence.