One of the big problems for Cloudflare users is that hackers may find a way to connect directly to your server’s IP address, instead of using the domain name. This would bypass Cloudflare and the protections it offers. Though the issue may affect any website, it is especially common on shared hosting, where website administrators often lack the capability to restrict access using server configuration files or IP tables.

Here’s a solution involving a Cloudflare Worker, a few .htaccess directives, and a Cloudflare Firewall Rule.

With a very simple Cloudflare Worker, we can add a request header that Cloudflare will send from its edge to your origin, and therefore won’t be visible to site visitors. The word “edge” refers to any of Cloudflare’s over 200 datacenters, and “origin” to the server at your hosting provider where your website is located.

As long as the site admin keeps header name and value are kept secret by the site admin, any request not coming through Cloudflare will trigger a rewrite condition at the origin server, and will be redirected back to, well, Cloudflare — where the Firewall Rule will block it.

The Worker

This worker was adapted from a recipe:

addEventListener('fetch', event => {
  event.respondWith(handleRequest(event.request))
})

/**
 * Send header to origin, allowing for
 * .htaccess to block requests
 * not coming from Cloudflare
 */

async function handleRequest(request) {
  // Make the headers mutable by re-constructing the Request.
  request = new Request(request)
  request.headers.set('Secret-Header', 'SeCrEt-k3y')

  return await fetch(request)
}

The header name and value could be anything permissible under Apache’s specifications. But you should refrain from using non-alphanumeric characters, other than perhaps a dash or an underscore, as they may lead to parsing issues during the chain of events.

To confirm the Worker is working properly and sending the specified header you set on Cloudflare, you can add an .htaccess directive to make your origin server return that request headers back as a response header. So if you name your header “Secret-Header”, you can try adding this directive to your .htaccess file:

Header echo Secret-Header

The above .htaccess directive takes no value, only the name of the header.

You can verify the presence of the header and value using your browser’s Developers Tools or similar web page inspection feature.

Once you are satisfied that the Worker is doing its job correctly, you should rename the header, and add the directives to your .htaccess file.

.htaccess

Place the following directives at the top of the.htaccess file.

# Route visitors not coming from Cloudflare to, well, Cloudflare
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# Both the header and the value should be kept secret
RewriteCond "%{HTTP:Secret-Header}" "!SeCrEt-k3y"
# Uncomment and edit w/ IP of services such as certs, cron, Softaculous etc
# RewriteCond "%{REMOTE_HOST}" "!^xxx\.xxx\.xxx\.xxx$"
RewriteRule .* "accessdenied.php" [R,L]
</IfModule>

These directives check every request to see if it comes with a request header named “Secret-Header” and if its value contains or not the string “SeCrEt-k3y”.

Requests coming from Cloudflare will have the proper header and key, and therefore will not be redirected.

But requests coming directly to the server, bypassing Cloudflare, will not have the header, or not have the key, and will be redirected to a non-existing file. You can call this file anything. It is named here accessdenied.php.

To verify that the .htaccess is working properly and is actively blocking attempts at reaching your site via IP address, you can use the following Curl command in your command line terminal:

# Replace example.com/1.2.3.4 with your domain and its IP address
curl -svo /dev/null https://example.com --connect-to ::1.2.3.4

The response may contain lots of information on the SSL negotiation, but should also include the following lines, confirming the redirect to the accessdenied.php URL:

< HTTP/2 302
< location: https://example.com/accessdenied.php

Firewall Rule

The name accessdenied.php points to a fictitious, non-existing file. You need to create a Firewall Rule so that Cloudflare blocks it should the bot follow the redirect set by the .htaccess directive above (not all bots follow redirects):

(http.request.uri.path eq "/accessdenied.php")

The Firewall Rule action should be set to Block.

The reason we prefer to redirect these requests to Cloudflare instead blocking them at the origin is because a redirect consumes less bandwidth and CPU than a 403 error page. We could rewrite the 403 error page down to a few bytes, but we prefer to keep the site functional for legit visitors, and that includes meaningful error pages whenever they face one.

Blocking intruders at Cloudflare also sends the right signal to probing hackers, that your site has the right configuration in place.

One consequence of this approach is that if you monitor your origin server logs, you’ll find it may contain both regular 302s as well as 302s for the URLs the occasional bot may be trying to reach.

The Cloudflare Firewall Events log will then have the entries for the blocks executed for URL "/accessdenied.php".

Legit Bots, Cronjobs, Etc

After implementing this solution, you may find that there are 302s on your server log that have no match on the Firewall Events log. This is because bots may be programmed not to follow redirects.

Also, it’s important to note that legit bots may be accessing directly your site to provide services such as cronjobs, certificate renewals etc. For these situations, there’s a bypass rule on the .htaccess directives above. You just need to uncomment and edit it to include the service’s own IP address.

Cost Issues

Cloudflare Workers is a paid service, and it starts at $5/month for the first 10 million requests. If you exceed that limit, you’ll be charged $0.50 per up to 1 million additional requests.

While this setup was tested in a Cloudflare Workers “free tier”, it may not be ideal to use the free tier on a production website, as it has some limits. Those monthly and hourly limits, if reached, will make your site break and generate 1015/1025 error pages for the duration of the rate-limiting period — something about which site owners have no control. Feel free to try it though. Just because it didn’t work on my site it doesn’t mean it won’t work on yours.

Keep in mind that Cloudflare will count each request, not each visit. If one visitors goes to a landing page on your site, that page’s HTML will request many other URLs for the images, fonts, CSS and JavaScript files, and so on.

Though the header will only be sent when Cloudflare doesn’t have the requested file in its cache, the worker will run (and therefore count towards your monthly limit) every time a visitor navigates on your website.

You can keep a tab of your requests by visiting the Workers Analytics panel at your dashboard on Cloudflare.

As with any usage-based Cloudflare product, I strongly recommend that you set a notification to avoid billing surprises. When properly set, Cloudflare will notify you via email once a certain volume of Workers requests have run. You can set an email notification by visiting the Billing section of your account on the Cloudflare dashboard. Make sure you include an email that doesn’t have delivery issues. Instead of using, for instance, the standard admin email of your WordPress installation, perhaps you should use Gmail, Outlook, or any other trusted third-party email provider.

This post is a revised version of a post originally posted on Cloudflare’s Community forum.

Comments

Please send any comments as replies to the tweet below. Click on the date of the tweet to open it on Twitter.

By cbrandt