Available — Local Digital Products
HCA · Studio
TR — Contact ↗
Technical Note 09 · Security

fail2ban is running and banning nobody

The jail is active, the service is healthy, the status output is clean — and the ban count has been zero for days. On a server running WordPress that is not good news; it almost always means the filter does not match your log format.

Server
OpenLiteSpeed
Symptom
Total banned: 0
Evidence
fail2ban-regex
Short answer

Stock WordPress filters assume Apache's combined log format, in which the line begins with the client IP. OpenLiteSpeed's log format is set per vhost, and in many installations the line begins with the vhost name ("example.com 1.2.3.4 - - [...") rather than the IP. A pattern anchored as ^<HOST> then matches nothing at all — and when fail2ban finds no matches it raises no error, it simply bans nobody. The fix is a pattern that tolerates an optional leading field, tested against your real log with fail2ban-regex.

The symptom

It is installed, the jail is defined, the service is running. You ask for status:

fail2ban-client status wordpress-ols

The output looks fine: jail enabled, file watched, no errors. But two lines stand out: Currently failed: 0 and Total banned: 0.

On a public site running WordPress, attempts against wp-login.php and xmlrpc.php arrive continuously. Those numbers staying at zero for days does not mean there is no attack; it means the filter cannot read your log lines.

The log format difference

Stock apache-* and community WordPress filters assume the line starts directly with the client IP:

1.2.3.4 - - [18/Sep/2026:07:14:22 +0000] "POST /wp-login.php HTTP/2" 200 1234

In OpenLiteSpeed the format depends on the logFormat line in the vhost configuration, and panel installations frequently prepend the vhost name:

"example.com 1.2.3.4 - - [18/Sep/2026:07:14:22 +0000] "POST /wp-login.php HTTP/2" 200 1234"

The difference is a single field, but for a pattern anchored at ^<HOST> it is fatal: there is no IP at the start of the line, so nothing ever matches.

What makes this worse is that two servers belonging to the same administrator can use two different formats. Copy a working filter from one to the other and it dies silently. Which is why the filter must be written against the actual line on that server, not against an assumption.

A resilient filter

The approach that handles both shapes is to accept an optional field at the start of the line. In /etc/fail2ban/filter.d/wordpress-ols.conf:

[Definition]
failregex = ^"?(?:\S+ )?<HOST> - -.*"(?:GET|POST|HEAD) [^"]*(?:wp-login\.php|xmlrpc\.php)
ignoreregex =
datepattern = %%d/%%b/%%Y:%%H:%%M:%%S %%z

Reading the pattern:

  • ^"? — some formats wrap the line in quotes; treat it as optional
  • (?:\S+ )? — swallows the vhost field if present, harmless if absent
  • <HOST> — fail2ban's placeholder for the captured IP
  • wp-login\.php|xmlrpc\.php — the two classic brute force targets
  • %%d/%%b/… — the percent signs are doubled on purpose: fail2ban treats a single % in its configuration as a variable and refuses to load the filter. Drop the line entirely and fail2ban detects the date on its own.

Both GET and POST are caught, because attackers probe the login page with GET first.

Never ship an untested rule

This is the most important sentence in the note: do not enable a filter you have not tested against your own log file. fail2ban ships a tool for exactly this:

fail2ban-regex /home/example.com/logs/example.com.access_log \
               /etc/fail2ban/filter.d/wordpress-ols.conf

Look at two numbers in the output:

Lines: 128934 lines, 0 ignored, 36925 matched, 92009 missed

If matched is zero your pattern does not work and there is no point enabling the jail. If you see thousands of matches, the pattern is right — and you have just learned how much attack traffic your site actually receives.

A high missed count is expected; those lines are ordinary traffic.

Jail configuration

In /etc/fail2ban/jail.local:

[wordpress-ols]
enabled  = true
port     = http,https
filter   = wordpress-ols
logpath  = /home/*/logs/*.access_log
maxretry = 5
findtime = 600
bantime  = 3600
ignoreip = 127.0.0.1/8 ::1

logpath accepts wildcards; on a multi-site server use a pattern instead of listing every vhost, and new sites are covered automatically.

After enabling, reload and confirm it is genuinely doing work:

fail2ban-client reload
fail2ban-client status wordpress-ols

Within a few hours Total banned should start climbing. If it is still zero, either the filter does not match or the log is not flowing — for the latter, check the log directory permissions.

The CDN trap

If the site sits behind a CDN, the connection reaching your server comes from a CDN node rather than the visitor, and the IP written to the log is the CDN's. In that case fail2ban bans the CDN instead of the attacker — and a single ban can lock out every visitor.

Two measures are needed together. First, exclude the CDN ranges from banning:

ignoreip = 127.0.0.1/8 ::1 173.245.48.0/20 103.21.244.0/22 ...

Second, make sure the real visitor IP is what gets logged. The CDN carries it in a header, and the web server must be configured to treat that header as the client IP. In OpenLiteSpeed this is the "use original IP header" option. Without it your bans hit the wrong target and your statistics are meaningless.

Summary

The dangerous thing about fail2ban is that a misconfigured install looks like a working one. Service up, jail active, no errors — and zero protection. So the last step of setup is not "I started the service" but "fail2ban-regex finds matches in my real log and the ban counter is climbing". Until it is verified, protection is an assumption.

Quick reference
Symptom
Jail active, Total banned: 0, no errors
Root cause
OLS log lines start with the vhost name; ^<HOST> never matches
Resilient pattern
^"?(?:\S+ )?<HOST> - - handles both shapes
Mandatory step
fail2ban-regex log filter — zero matched means a dead rule
Targets
wp-login.php and xmlrpc.php, GET and POST together
Multi-site
Use a wildcard: logpath = /home/*/logs/*.access_log
Behind a CDN
CDN ranges must be in ignoreip or you ban the CDN itself
Prerequisite
No log flow means a blind fail2ban — verify permissions first
Frequently Asked Questions

About fail2ban and OpenLiteSpeed.

The jail looks active but nothing is ever banned. What is wrong?

Almost always the filter pattern does not match your log format. In OpenLiteSpeed logs the line often begins with the vhost name, while stock WordPress filters assume it begins with the IP. fail2ban raises no error when nothing matches — it simply bans nobody. Test with fail2ban-regex; if matched is zero, that is your answer.

How do I test a filter properly?

Run fail2ban-regex against your real access log and your filter file, then read the matched count. Zero means the pattern is not working. A high number means the pattern is right, and as a bonus you learn the true volume of brute force traffic hitting your site.

My site is behind Cloudflare. Is fail2ban still useful?

Without extra configuration it does more harm than good. The logs record the CDN's IP, so fail2ban bans the CDN rather than the attacker, and one ban can lock out all visitors. Add the CDN ranges to ignoreip and configure the web server to log the real visitor IP from the CDN's header.

Is a WordPress security plugin not enough on its own?

They work at different layers. A plugin only sees the request after PHP has already run, so every attempt still consumes server resources. fail2ban drops the IP at the firewall and the request never reaches PHP at all. Under heavy brute force traffic the difference in resource usage is substantial.

Availability and Quotes

Have an idea?
Half a sentence is enough.