Lite free · Pro $59 · Craft CMS 5
Your firewall can't see a failed login
A wrong password and a right one both come back 200, so fail2ban has nothing to match. Craft knows the difference. Fail2Ban writes it down in the format fail2ban already parses, and the firewall does the banning, before PHP is involved at all.
What fail2ban reads
One line per event, as craft(host)[pid]: — the shape fail2ban's common.conf already understands, and the same shape wp-fail2ban uses, so anyone who runs both knows these lines on sight. The address is always last.
2026-08-18T09:14:02+0000 craft(example.com)[4821]: Authentication failure for jo@example.com from 203.0.113.7
2026-08-18T09:14:04+0000 craft(example.com)[4823]: Authentication attempt for unknown user root from 203.0.113.7
2026-08-18T09:14:06+0000 craft(example.com)[4826]: Blocked authentication attempt for admin from 203.0.113.7
2026-08-18T09:14:09+0000 craft(example.com)[4831]: Probe for /wp-login.php from 203.0.113.7
Features
The sentence is the plugin's job. The ban is fail2ban's.
Every login outcome, told apart
Wrong password, unknown user, blocked username, cooldown, locked, suspended, failed elevated session, invalid reset token. Each is logged accurately even when Craft's preventUserEnumeration tells the visitor "invalid credentials" for all of them.
Filters that can't drift
Each event owns one template, and that one string writes the log line and builds the failregex. A check renders every line and matches it against its own filter: on the Setup screen, in fail2ban/status, and as a CI-friendly exit code.
Filters and jails, generated
Four jails with sensible numbers, from one strike for things nobody does by accident to five tries for a forgotten password. Download them from Setup or write them with one console command.
- craft-hard: 1 in 1 hour, banned 24 hours
- craft-auth: 5 in 10 minutes, banned 1 hour
- Pro: craft-probe and craft-flood
Usernames that don't exist here
admin, root and test come from a list, not a colleague. A blocked name is a one-strike ban. By default the list only applies to names with no real account, so an install whose administrator is called admin is never locked out.
Probes for software you don't run
Pro. /wp-login.php, /xmlrpc.php, /.env and /vendor/phpunit are requests nobody makes to a Craft site by accident. Paths match as whole segments, so an article about wp-admin is just somebody reading.
Nobody else picks who gets banned
The address comes off the socket, not from a header a visitor can type. Proxies are trusted only when named, and read right to left. Nothing a visitor submits can add a line to the log.
It tells you when a proxy is in the way
Behind a load balancer or CDN, every event would name the proxy, and the first ban would block every visitor. Setup and fail2ban/status warn about exactly that before you install the jails.
No fail2ban? Pro blocks in PHP
For hosts with no shell, the built-in blocker enforces the same jails with one indexed lookup per request, and emails you once per banned address. Use fail2ban where you can. The blocker is for where you can't.
Install it, then prove it works
Copy the generated files, reload fail2ban, and check the filter against real lines before an attacker does it for you. configs/verify exits non-zero if any filter no longer matches its line.
sudo php craft fail2ban/configs/write --directory=/etc/fail2ban
sudo fail2ban-client reload
php craft fail2ban/events/test # one line per event, from 203.0.113.7
fail2ban-regex storage/logs/fail2ban.log /etc/fail2ban/filter.d/craft-auth.conf
php craft fail2ban/status # channels, jails, warnings, filter agreement
php craft fail2ban/configs/verify # for CI
Frequently Asked Questions
Questions worth answering before you install it.
fail2ban matches lines in log files, and your web server's log shows a 200 for a wrong password and a 200 for a right one. There is no line to match. Craft knows the difference, so this plugin writes the line.
It protects one account. fail2ban protects the site from one address, across every account, at the firewall. A credential-stuffing run tries one password against ten thousand accounts and never trips any account's cooldown.
Not by sending a header or typing into a form. The address comes off the socket unless you name your proxies, the forwarded chain is read right to left, and anything a visitor submits is flattened onto one line before it is written. A site behind a proxy the plugin hasn't been told about is the one risky setup, and Setup warns about it.
Pro's built-in blocker enforces the same jails in PHP. It's the weaker option, since it still spends a PHP worker on every attempt, but it's much better than nothing.
No. It does work only when Craft raises a security event, or on an error response in Pro. Nothing on the 404 path starts a session, so it never adds a Set-Cookie header to a cacheable response.
By default, yes, because "five failures for one account" is worth seeing. Turn on Hash usernames in the log and the log gets a stable, keyed, one-way hash instead. Query-string values are never logged.
Lite is free and covers the whole authentication story: every login, token and reset event, all three channels, blocked usernames, the generated filters and jails, and the console. Pro is $59 one-off, with an optional $29/year renewal for updates. It adds probes, 404 floods, enumeration, GraphQL failures, the built-in blocker and ban emails.
Craft CMS 5.3+ and PHP 8.2+. There's no build step, no runtime dependency beyond Craft, and no outbound requests.
Let the firewall do the banning
Lite is free and covers every authentication event. Pro is $59 one-off with a $29/year renewal, and adds probes, 404 floods, enumeration and a built-in blocker for hosts without fail2ban.