Stop WordPress Login Attacks Before They Reach Your Site
Every WordPress site has a login page at the same address, /wp-login.php, and bots know it. On our own production server, a single IP address tried to log in 1,420 times in two months. Two others tried 860 and 742 times, and more than 40 different addresses joined in. None of them got in, but every attempt cost the server work, and one weak password would have been enough. fail2ban is a small tool that watches for this and shuts the door on an attacker after a handful of tries.
Quick Summary: Brute-force bots try passwords on your WordPress login page all day. fail2ban reads the server’s access log, spots an address that keeps failing, and blocks it at the firewall for a while, so it cannot even load your site. It needs no WordPress plugin and no maintenance once it is set up. On a Trellis server it is already installed, but the WordPress protection is switched off until you turn it on. Ask your host whether something like it runs on your server.
Why login attacks matter even when they fail
A brute-force attack is a bot guessing usernames and passwords, one after another, until something works. Most of these bots are not aimed at you. They work through lists of millions of sites and try the same common passwords on each one. That has three costs for a small business site:
- Risk. One reused or simple password on any account with editing rights is enough for a takeover: spam pages, a redirect to a scam site, or malware served to your visitors. For a business that means lost orders, a browser or Google warning on your own site, and days of cleanup while customers see a broken page.
- Speed. Each login attempt runs PHP and queries the database, just like a real visit. Thousands of them slow the site down for your actual customers.
- Noise. Security plugins that block at the WordPress level still let every request reach WordPress first, so the server does the work before the plugin says no.
What fail2ban does
fail2ban runs on the server, outside WordPress. It reads the web server’s access log, the list of every request the site receives. When one address fails to log in too many times within a short window, fail2ban adds a firewall rule that blocks that address. After the ban time runs out, the rule is removed again.
Because the block happens at the firewall, a banned bot never reaches WordPress at all. There is no plugin to update, nothing in the WordPress dashboard, and nothing that slows down normal visitors. The bans are temporary on purpose: attackers change addresses anyway, and a short ban means a real person who mistyped their password a few times is only locked out for minutes, not forever.
What to do as a site owner
- Use strong, unique passwords for every account that can edit the site, and remove old accounts nobody uses. fail2ban slows guessing down; it does not make a weak password safe.
- Ask your host what protects the login page. A good answer names something that blocks at the server or firewall level, not only a plugin. A host that cannot answer is a warning sign. Three questions worth sending: Do you limit failed logins at the server? How long is a ban, and can my own team get locked out? Do I get told when an attack is blocked?
- Switch on two-factor login. Even a stolen password then gets an attacker nowhere. It is the strongest single step after a unique password, and it takes ten minutes to set up.
- If you run your own server, turn it on. fail2ban is free and common on Linux servers. On Trellis, the server setup we use, it takes one setting and one command, shown below.
How we set it up on our own servers
Imagewize runs on a Trellis server on Hetzner. Trellis installs fail2ban and protects SSH out of the box, but its WordPress login jail ships switched off. We turned it on with a ban after six failed logins within ten minutes, lasting fifteen minutes. The first attacker was caught within seven hours.
One thing caught us out. Jasper works from changing locations and VPN exits, and the filter we started with sometimes banned him while he was simply using the site. The cause was a pattern that matched wp-login.php anywhere in a log line, including the address of the page someone came from. Tightening it to look only at the requested page itself fixed the false bans. We still exempt a VPN range, because one ban did land during a two-factor reset, so treat an exemption list as a safety valve, not a substitute for a correct filter.
For the developers: turning on the WordPress jail in Trellis
The jails live in group_vars/all/security.yml. Set enabled to "true" on the login jail. A per-jail maxretry overrides the global one, so SSH can stay strict while the login page allows a few typos.
# group_vars/all/security.yml
fail2ban_bantime: 900 # 15 minutes
fail2ban_maxretry: 3 # global default, used by the sshd jail
fail2ban_services_custom:
- name: wordpress_wp_login
filter: wordpress-wp-login
enabled: "true"
port: http,https
logpath: "{{ www_root }}/**/logs/access.log"
maxretry: 6 # more forgiving for real users
Apply only the fail2ban role, not a full provision:
trellis provision --tags fail2ban production
Check the jail as the admin user (sudo needs a password):
ssh -t admin@example.com "sudo fail2ban-client status wordpress_wp_login"
Skip the wordpress_xmlrpc jail if Nginx already refuses xmlrpc.php. Run curl -I https://example.com/xmlrpc.php: a 403 or an empty 444 response means there is nothing left for that jail to catch.
For the developers: the filter that stopped banning us
A failed WordPress login returns 200 (the form is shown again), while a successful one redirects with a 302. So the filter looks for a POST to wp-login.php that came back 200. The important part is anchoring it to the quoted request field of the Nginx log line:
# filter.d/wordpress-wp-login.conf
[Definition]
failregex = ^<HOST> .* "POST [^"]*wp-login\.php[^"]*" 200
An unanchored pattern such as wp-login\.php.* 200 also matches a line whose referrer contains wp-login.php, so a logged-in user clicking around right after logging in can build up “failures”. Test a filter against a real log before trusting it:
sudo fail2ban-regex /srv/www/example.com/logs/access.log \
/etc/fail2ban/filter.d/wordpress-wp-login.conf
If you lock yourself out anyway, unban from another connection or the server console:
sudo fail2ban-client set wordpress_wp_login unbanip 203.0.113.7
Our full notes, including permanent Nginx blocks for persistent attackers, are in the wp-ops fail2ban guide.
Login attacks are not the only bots that hammer a WordPress site. Our post on rate limiting contact form spam with Nginx covers the other common one, and hardening PHP-FPM pools on Trellis covers what happens when too many requests get through at once. fail2ban needs control of the server, so it is not an option on shared hosting. If you would rather not run any of this yourself, that is what managed WordPress hosting is for.
Frequently asked questions
- Do I still need a security plugin if I use fail2ban? fail2ban covers login guessing at the server level, which is where it is cheapest to stop. A plugin can still be useful for things like two-factor login. They do different jobs.
- Can fail2ban lock out my own team? Only after several wrong passwords in a short time, and only for the ban period, fifteen minutes in our setup. Office or fixed IP addresses can be put on an exemption list.
- Does fail2ban slow down my website? No. It reads the log in the background, and blocked addresses are refused by the firewall before they reach the web server, so it takes load off the site rather than adding it.
- Does hiding or renaming wp-login.php stop brute force attacks? It cuts the noise from simple bots, but it is not a real defense. Attackers find the new address, and the login still accepts unlimited guesses. Limiting failed logins and using two-factor login are what actually stop the attack.
- Can I use fail2ban on shared hosting? No. It needs control of the server’s firewall. On shared hosting, ask your host what they run instead.
Want Your Server Protected Without Doing It Yourself?
We offer managed WordPress hosting built on Trellis, with fail2ban, firewall rules and server security updates handled for you, Nginx, Redis object cache, automated deployments via Ansible, and Bedrock structure on Hetzner EU. No shared hosting, no page builders, no surprises.
- Trellis + Bedrock on Hetzner EU (Frankfurt / Helsinki)
- Nginx + FastCGI caching + Redis object cache
- Automated deployments via Ansible, SSL via Let’s Encrypt
- From €49/month, or €65/hour for one-off server work