Fail2Ban for Craft CMS

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.

Fail2Ban

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.

text
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.

bash
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.

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.