When WordPress Asks Cloudflare for Help

A few years ago we started noticing something rather annoying. Some of the VPS servers hosting our clients’ WordPress websites would suddenly slow to a crawl. Occasionally they’d become completely unresponsive.

Sometimes the cause was perfectly legitimate traffic. A mention on television, for example, can bring an unexpected crowd to a website. Mary Berry appearing at the Mermaid Inn in Rye has given us a taste of that particular problem (which no doubt will happen on the repeats in a few weeks)!

But more often than not, we discovered the server was being hammered by bots. Not people looking at photos or making bookings. Automated requests probing for vulnerabilities, files that shouldn’t be public and anything else they could get their hands on.

We already had firewalls dealing with the requests. But Apache still had to receive them, and the server was still doing work we’d rather it didn’t have to do.

So we wondered … could a WordPress website tell Cloudflare when it needed help?

Turns out it could. And after several years of tinkering, we’ve built a plugin that does exactly that.

The short version

Our new (currently unnamed) WordPress plugin watches for two things: a server that’s struggling and IP addresses identified as attackers by Wordfence or our own Deny All Firewall.

If the server is overloaded, the plugin asks Cloudflare to turn on extra protection. When the server has recovered, it restores the previous security setting. Meanwhile, IP addresses caught making particularly suspicious requests can be blocked at Cloudflare for 24 hours, before their next request even reaches the server.

And the most satisfying bit? It’s working on real websites.

The idea in three steps

  1. WordPress spots trouble.
    Either the server is struggling or one of our existing firewalls has identified an attacker.
  2. Cloudflare steps in.
    It challenges incoming traffic when necessary or blocks known attacking IP addresses.
  3. The server gets a break.
    Fewer unwanted requests reach Apache, and the extra protection is removed when it’s no longer needed.

Nothing here requires somebody to notice an attack, sign into Cloudflare and start clicking buttons.

What is Cloudflare, and why block requests there?

Cloudflare sits between visitors and a website’s server. When Cloudflare blocks a request, that request doesn’t need to be passed on to Apache and WordPress. Our server avoids handling that particular request altogether. It doesn’t mean the website is invulnerable, but it means a lot of obvious rubbish can be stopped earlier.

First we taught WordPress when to ask for help

The original idea had nothing to do with maintaining a list of attackers. We just wanted a website to recognise when its server was under pressure.

PHP can read a server’s load averages, and on most systems we can also determine how many CPU cores are available. That’s important because a load average of four means something rather different on a single-core server than it does on a machine with eight cores.

Our plugin checks the load on each WordPress page request. If both the one-minute and five-minute averages exceed the number of available cores, we consider the load sustained. If the one-minute average exceeds twice the number of cores, that’s serious enough to act on immediately.

It then tells Cloudflare to enable Under Attack mode.

When all three load averages (one, five and fifteen minutes) fall below 75% of the core count, the plugin considers the server recovered and restores the previous security setting.

So on a four-core VPS, a one-minute load above eight is enough to trigger protection; sustained one- and five-minute loads above four can trigger it too. The different recovery threshold helps stop the protection constantly switching on and off.

What is a server load average?

Load average tells us roughly how much work is running or waiting for CPU time, including some tasks waiting for system resources. It’s not a percentage of CPU usage. We compare the one-, five- and fifteen-minute averages with the server’s CPU core count, so a small VPS isn’t judged against the same absolute numbers as a much larger server.

What does Cloudflare’s Under Attack mode do?

It puts an additional browser verification challenge in front of visitors to help filter out automated attacks. Genuine visitors may see a short checking-your-browser screen before continuing. That’s an inconvenience, and it can affect automated services, but we’d rather visitors occasionally wait through a challenge than arrive at a website that’s completely unresponsive. Cloudflare describes it as a measure for application-layer attacks, not something to leave enabled permanently.

We can also activate Under Attack mode if the firewalls detect 50 new, distinct attacking IP addresses within ten minutes. That threshold hasn’t actually been reached in production yet, so we may adjust it as we learn more. The protection currently lasts for at least 15 minutes after that sort of surge, and longer if the server remains overloaded.

The server-load protection, on the other hand, is already triggering on many of the websites running the plugin, often at least once a day. That’s a dozen or so installations at the moment.

Of course, the plugin has to get a chance to run before it can react. It can’t rescue a server that has already stopped executing PHP entirely. The aim is to intervene before things get that bad.

Then we realised we could stop the attackers coming back

We’ve been developing Deny All Firewall for some time. Unlike Wordfence, which recognises attacks and blocks them, Deny All Firewall takes a more restrictive approach: it denies requests that don’t fit what the website should legitimately be serving, where it can.

Both plugins give us useful evidence of malicious activity. Why leave the same IP address free to keep probing the website after it’s already been caught trying something particularly nasty?

Our new plugin collects qualifying IP addresses from Wordfence’s WAF blocks and Deny All Firewall’s high-confidence threat log, then sends them to a Cloudflare IP list used by a blocking rule.

Deny All Firewall doesn’t flag every 403 response as an attacker. Its standalone 403 handler, which doesn’t load WordPress, looks for very specific requests: attempts to retrieve .env files, Git repository details, cloud credentials and Terraform state files; known PHP web shells; suspicious PHPUnit paths; certain direct requests for WordPress internal PHP files; and attempts to reach sensitive files using directory traversal.

In other words, not somebody making a typo in a URL. Somebody looking for trouble.

Wordfence is broader: at present, the new plugin includes all IP addresses blocked by its WAF. That means false positives are possible, and we’ve already had one ourselves while configuring Matomo. We removed our IP from Cloudflare and carried on. It’s a shield against bot activity, not a claim that our detection is infallible.

The Cloudflare block lasts 24 hours after the most recent detection. If the same attacker turns up again, the clock resets. Once the period expires, the plugin removes the address from the list.

How does WordPress tell Cloudflare what to do?

Through Cloudflare’s API, which lets software change selected Cloudflare settings using an authorised token. Our plugin uses it to manage an IP list and its blocking rule, and to switch Under Attack mode on and off. It does the syncing in background requests, records failures and retries when necessary. It doesn’t need to poll Cloudflare every minute to see whether anything has happened.

There are some safeguards too. We exclude known public IP addresses belonging to the server, keep track of pending updates and avoid issuing competing protection changes at the same time. For now we run each installation against a separate Cloudflare account so two websites don’t try to manage the same attacker list and undo each other’s work.

So … does it actually work?

Here’s the lovely bit.

We deployed the IP blocking on four websites sharing a single VPS. Eight hours later, the client reported the following Cloudflare-blocked request counts:

WebsiteRequests blocked
albumen-gallery.com1,650
albumen-publishing.com278
williamstewart.me2,020
melbournephotobookcollective.org1,680
Total5,628

That’s over 5,600 requests in eight hours that Cloudflare handled without passing them on to Apache.

And it isn’t confined to that server. In separate 24-hour reports, we saw around 3,940 blocked requests on webd.uk and 3,500 on tourist.org.uk.

Those figures are rounded, and they’re counts of blocked requests, not unique hackers. They don’t tell us exactly how much CPU time we’ve saved, nor prove every blocked request would otherwise have caused a problem.

But thousands of requests that our servers no longer have to process? We’ll take that!

What happens next?

There’s still work to do, and we’re not pretending this is a silver bullet. Cloudflare can challenge legitimate visitors when Under Attack mode is on, and a firewall can occasionally mistake an innocent request for an attack. That’s why we have recovery rules, expiring bans and ways to correct a mistake.

We also don’t want a security plugin consuming the very resources it’s supposed to save. Most of the work happens only when there’s something to do, with background syncing rather than constantly hammering the Cloudflare API.

For now the plugin is running on our own and clients’ websites, but it isn’t publicly available. It’s taken a while to get here, and if we decide to release it, it’ll most likely be a premium plugin rather than something we put straight into the WordPress plugin repository.

If you’d be interested in using it, get in touch. We’d be happy to hear from you.

The original aim was simple: let WordPress tell Cloudflare when the server needs help. Along the way we realised we could also stop an awful lot of the constant barrage of rubbish before it even reaches us.

And after all the experimenting, testing and occasional head-scratching, the best bit is still the simplest.

It works!

Comments …

Leave a Reply

Your email address will not be published. Required fields are marked *