Security

Fail2Ban Setup Guide: Protect SSH, Nginx, and WordPress from Brute Force

April 08, 2026

Back to Blog

The Scale of Brute Force Attacks in 2026

Every server connected to the internet is under constant bombardment. The moment you provision a VPS and assign it a public IP address, automated scanners begin probing your SSH port, testing default credentials, and looking for vulnerable web applications. This is not a hypothetical threat — it is a statistical certainty.

50,000+
Average SSH login attempts per server per day
93%
Of breached servers had brute force as initial vector

Research from security firms consistently shows that an unprotected server receives between 30,000 and 100,000 SSH brute-force attempts daily. WordPress login pages attract thousands of credential-stuffing attempts per hour. Nginx authentication endpoints are continuously hammered by botnets. Without an automated defense mechanism, it is only a matter of time before an attacker finds a weak password or exploits a timing vulnerability.

Real-world impact: A successful brute force attack does not just compromise one account. Attackers escalate privileges, install rootkits, deploy cryptocurrency miners, and use your server as a launching pad for further attacks. A single compromised server can result in your entire IP range being blacklisted.

What Is Fail2Ban and How Does It Work?

Fail2Ban is an intrusion prevention framework written in Python that monitors log files for patterns of malicious behavior and automatically updates firewall rules to block offending IP addresses. It operates on a simple but powerful principle: if someone fails to authenticate too many times within a defined period, they are banned.

Log Files
auth.log, access.log
Fail2Ban
Pattern Matching
Filter Match
Regex on log lines
Action
iptables/nftables ban

The architecture consists of three core components:

Filters

Regular expressions that match failed authentication patterns in log files. Each filter knows what a failed login looks like for a specific service. For example, the SSH filter matches patterns like Failed password for and Invalid user from /var/log/auth.log.

Jails

A jail combines a filter with an action and defines the parameters: which log file to monitor, how many failures trigger a ban (maxretry), how long to look back (findtime), and how long to ban (bantime). Each jail protects one service.

Actions

What happens when a ban is triggered. The most common action is adding a firewall rule (iptables or nftables) that drops all packets from the offending IP. Actions can also send email notifications, update DNS blacklists, or execute custom scripts.

Installing Fail2Ban

Fail2Ban is available in the default Ubuntu repositories and can be installed with a single command. After installation, it starts automatically and enables the SSH jail by default.

$ sudo apt update && sudo apt install fail2ban -y
Reading package lists... Done
Setting up fail2ban (1.1.0-1) ...
Created symlink /etc/systemd/system/multi-user.target.wants/fail2ban.service

$ sudo systemctl status fail2ban
● fail2ban.service - Fail2Ban Service
Active: active (running)
Main PID: 12847 (fail2ban-server)
Important: Never edit /etc/fail2ban/jail.conf directly — it gets overwritten during package upgrades. Always create your customizations in /etc/fail2ban/jail.local or as individual files in /etc/fail2ban/jail.d/.

Configuring the SSH Jail

SSH protection is the most critical jail and should be your first configuration. The default settings are often too lenient for production servers. Here is a hardened SSH jail configuration:

$ sudo nano /etc/fail2ban/jail.local

[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
banaction = nftables-multiport
banaction_allports = nftables-allports
ignoreip = 127.0.0.1/8 ::1

[sshd]
enabled = true
port = ssh
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
findtime = 600
bantime = 86400

Let us break down each parameter to understand exactly what it does:

ParameterValueMeaning
maxretry3Ban after 3 failed attempts
findtime600Count failures within a 10-minute window
bantime86400Ban duration: 24 hours (86400 seconds)
banactionnftables-multiportUse nftables for blocking specific ports
ignoreip127.0.0.1/8Never ban localhost connections
Custom SSH port: If your SSH runs on a non-standard port (e.g., 2847), change the port directive to port = 2847. Fail2Ban needs to know which port to block in the firewall rule.

Progressive Ban Times

Fail2Ban supports incremental ban times for repeat offenders. This is extremely effective against persistent attackers who return after their initial ban expires:

[sshd]
enabled = true
port = ssh
logpath = /var/log/auth.log
maxretry = 3
findtime = 600
bantime = 3600
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 604800

With this configuration, the first ban lasts 1 hour, the second ban lasts 2 hours, the third lasts 4 hours, and so on — doubling each time up to a maximum of 7 days (604800 seconds). This means persistent attackers face increasingly severe penalties while legitimate users who mistype their password once recover quickly.

Nginx Jails: Protecting Your Web Server

SSH is just one attack vector. Your web server faces its own set of threats: authentication brute forcing, bot crawling, script injection attempts, and more. Fail2Ban ships with several Nginx filters that you can enable immediately.

Nginx HTTP Authentication

If you use HTTP Basic Auth to protect admin areas, staging sites, or internal tools, this jail catches repeated authentication failures:

[nginx-http-auth]
enabled = true
port = http,https
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 5
findtime = 300
bantime = 3600

Nginx Bot Search

Bots constantly scan for vulnerable scripts, backup files, and admin panels. They request paths like /admin, /phpmyadmin, /.env, and /wp-admin even on servers that do not run those applications. The nginx-botsearch jail bans IPs that trigger too many 404 errors on suspicious paths:

[nginx-botsearch]
enabled = true
port = http,https
filter = nginx-botsearch
logpath = /var/log/nginx/access.log
maxretry = 5
findtime = 300
bantime = 86400

Nginx No-Script

This jail targets scanners looking for executable scripts (CGI, PHP, ASP) in locations where they should not exist. It is particularly useful for static sites or sites with strictly controlled PHP locations:

[nginx-noscript]
enabled = true
port = http,https
filter = nginx-noscript
logpath = /var/log/nginx/access.log
maxretry = 3
findtime = 300
bantime = 86400

WordPress Brute Force Protection

WordPress is the most targeted CMS on the internet. The wp-login.php and xmlrpc.php endpoints are continuously hammered by credential-stuffing bots. Since Fail2Ban does not ship with a WordPress filter by default, you need to create a custom one.

1

Create the WordPress Filter

This filter matches POST requests to wp-login.php and xmlrpc.php that result in authentication failures:

$ sudo nano /etc/fail2ban/filter.d/wordpress-login.conf

[Definition]
failregex = ^<HOST> .* "POST /wp-login\.php .* (200|302)
^<HOST> .* "POST /xmlrpc\.php .* 200
ignoreregex =
2

Create the WordPress Jail

Now create a jail that uses this filter and monitors your Nginx access logs:

[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/nginx/*access.log
maxretry = 5
findtime = 300
bantime = 43200
3

Test the Filter

Always validate your filter regex before activating the jail:

$ fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/wordpress-login.conf

Results
========
Failregex: 47 total
Ignoreregex: 0 total
Lines: 15820 lines, 0 ignored, 47 matched, 15773 missed

Writing Custom Filters

The real power of Fail2Ban lies in its extensibility. You can write custom filters for any application that writes authentication events to log files. Here is the anatomy of a filter and how to build one from scratch.

Filter Regex Syntax

Fail2Ban uses Python regular expressions with one special placeholder: <HOST>, which matches an IP address (both IPv4 and IPv6). Every failregex line must contain <HOST> to tell Fail2Ban which IP to ban.

# Example: Custom filter for a Node.js app
[Definition]

# Match lines like: 2026-03-15 10:23:45 WARN Login failed for user admin from 192.168.1.50
failregex = Login failed for user \S+ from <HOST>

# Exclude internal monitoring IPs
ignoreregex = <HOST> .* monitoring-healthcheck

Testing Filters with fail2ban-regex

Before deploying any custom filter, test it against actual log data. The fail2ban-regex command simulates filter matching without affecting your firewall:

$ fail2ban-regex /var/log/myapp/auth.log /etc/fail2ban/filter.d/myapp.conf --print-all-matched

Matched lines:
2026-03-15 10:23:45 WARN Login failed for user admin from 203.0.113.50
2026-03-15 10:23:47 WARN Login failed for user admin from 203.0.113.50
2026-03-15 10:23:49 WARN Login failed for user root from 203.0.113.50

Managing Bans: Essential Commands

Knowing how to check status, ban, and unban IPs is critical for day-to-day operations. Here are the commands you will use most frequently:

CommandDescription
fail2ban-client statusList all active jails
fail2ban-client status sshdShow banned IPs for SSH jail
fail2ban-client set sshd unbanip 1.2.3.4Manually unban an IP
fail2ban-client set sshd banip 1.2.3.4Manually ban an IP
fail2ban-client reloadReload configuration without restart
fail2ban-client get sshd bantimeCheck current ban time setting
fail2ban-client get sshd maxretryCheck current max retry setting
$ sudo fail2ban-client status sshd
Status for the jail: sshd
|- Filter
| |- Currently failed: 2
| |- Total failed: 18473
| `- File list: /var/log/auth.log
`- Actions
|- Currently banned: 47
|- Total banned: 2891
`- Banned IP list: 203.0.113.5 198.51.100.23 ...

iptables vs nftables Backend

Modern Linux distributions (Ubuntu 22.04+) have migrated from iptables to nftables as the default firewall framework. Fail2Ban supports both, but you need to configure the correct backend to avoid conflicts.

iptables (Legacy)

  • Default on older distributions
  • Chain-based rule management
  • banaction = iptables-multiport
  • Works but generates deprecation warnings on newer systems

nftables (Modern)

  • Default on Ubuntu 22.04+
  • Set-based ban management (faster lookups)
  • banaction = nftables-multiport
  • Better performance with large ban lists
Recommendation: On Ubuntu 22.04 and newer, always use nftables-multiport as your ban action. It uses nftables sets which provide O(1) lookup time regardless of how many IPs are banned, compared to iptables chains which have O(n) lookup time.

To configure nftables globally for all jails, add these lines to the [DEFAULT] section of your jail.local:

[DEFAULT]
banaction = nftables-multiport
banaction_allports = nftables-allports
chain = input

Email Notifications

You should know when bans are triggered, especially for jails protecting critical services. Fail2Ban can send email notifications on ban and unban events using the sendmail or mail action:

[sshd]
enabled = true
port = ssh
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400
action = %(action_mwl)s

[DEFAULT]
destemail = [email protected]
sender = [email protected]
mta = sendmail

The action types control how much information you receive:

ActionBehavior
action_Ban only, no notification
action_mwBan + email with whois info
action_mwlBan + email with whois info + relevant log lines

Persistent Bans Across Restarts

By default, Fail2Ban loses all bans when the service restarts (which happens during package upgrades or server reboots). To make bans persistent, you need to enable the ban database:

$ sudo nano /etc/fail2ban/jail.local

[DEFAULT]
# Enable persistent ban database
dbfile = /var/lib/fail2ban/fail2ban.sqlite3
dbpurgeage = 604800

The dbpurgeage parameter controls how long ban records are kept in the database. Setting it to 604800 seconds (7 days) means that bans are remembered across restarts for up to one week, and the incremental ban time feature can reference previous bans.

Complete Production Configuration

Here is a complete, production-ready jail.local that combines everything we have discussed into a single configuration file:

# /etc/fail2ban/jail.local — Production Configuration

[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 5
banaction = nftables-multiport
banaction_allports = nftables-allports
ignoreip = 127.0.0.1/8 ::1
destemail = [email protected]
sender = [email protected]
mta = sendmail
dbfile = /var/lib/fail2ban/fail2ban.sqlite3
dbpurgeage = 604800

[sshd]
enabled = true
port = ssh
logpath = /var/log/auth.log
maxretry = 3
bantime = 86400
bantime.increment = true
bantime.factor = 2
bantime.maxtime = 604800
action = %(action_mwl)s

[nginx-http-auth]
enabled = true
port = http,https
logpath = /var/log/nginx/error.log
maxretry = 5
bantime = 3600

[nginx-botsearch]
enabled = true
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
bantime = 86400

[wordpress-login]
enabled = true
port = http,https
filter = wordpress-login
logpath = /var/log/nginx/*access.log
maxretry = 5
findtime = 300
bantime = 43200

Monitoring and Troubleshooting

After deploying Fail2Ban, continuous monitoring is essential to ensure it is working correctly and not producing false positives that lock out legitimate users.

Checking Fail2Ban Logs

$ sudo tail -f /var/log/fail2ban.log
2026-03-15 14:23:01,234 INFO [sshd] Found 203.0.113.50 - 2026-03-15 14:23:01
2026-03-15 14:23:15,567 INFO [sshd] Found 203.0.113.50 - 2026-03-15 14:23:15
2026-03-15 14:23:29,890 INFO [sshd] Found 203.0.113.50 - 2026-03-15 14:23:29
2026-03-15 14:23:30,123 NOTICE [sshd] Ban 203.0.113.50

Common Issues and Solutions

Problem: Fail2Ban not banning despite failed logins

Cause: Log file path mismatch or incorrect filter regex.

Solution: Verify the logpath in your jail matches the actual log file location. Test with fail2ban-regex.

Problem: Legitimate users getting banned

Cause: maxretry too low or findtime too long.

Solution: Add trusted IPs to ignoreip. Increase maxretry to 5 for non-critical jails.

Problem: Bans not working (IP can still connect)

Cause: Firewall backend mismatch (iptables rules on nftables system).

Solution: Switch banaction to match your firewall. Check with nft list ruleset.

Fail2Ban with Panelica

Panelica ships with Fail2Ban pre-configured — SSH, web server, and application-level jails are active from installation. The panel provides a dedicated Security section where you can manage bans, whitelist trusted IPs, adjust jail parameters, and review banned addresses with their ban history. Jails for SSH, Nginx authentication, bot scanning, and WordPress are all enabled and tuned with sensible defaults. When you need to unban an IP or add a custom jail, the graphical interface eliminates the need to edit configuration files manually. All changes are applied instantly without restarting the Fail2Ban service.

Best Practices Summary

  • Always use jail.local for customizations, never edit jail.conf
  • Test every custom filter with fail2ban-regex before deploying
  • Use nftables backend on Ubuntu 22.04+ for better performance
  • Enable incremental ban times for SSH to punish persistent attackers
  • Add your own IP to ignoreip to prevent self-lockout
  • Enable the SQLite database for persistent bans across restarts
  • Monitor /var/log/fail2ban.log regularly for false positives
  • Configure email notifications for critical jails (SSH, database)
  • Set aggressive ban times (24h+) for SSH but moderate times (1h) for web jails
  • Keep Fail2Ban updated — newer versions include better filters and performance
Share: