Configure mutual TLS with Cloudflare and Platform.sh

Recently, I’ve started building sites with WordPress again. It’s been some years since my last. Given I work for a DevOps provider, I figure I’d configure a proper modern stack.

On a proper modern stack, having a solid WAF is very important, especially with WordPress. Cloudflare is an amazing WAF. It’s also very good at working with WordPress. To fully protect my site, I had to configure mutual TLS. While all the documentation and features existed, it wasn’t clear what steps were needed.

In this article, I cover the exact steps required to configure mutual TLS with Cloudflare and Platform.sh. Or, origin cloaking between Cloudflare and Platform.sh.

Before you start

So before you dive head in, there’s a few things you need to be aware of.

You can’t use IP filtering to set this up, you have to use mTLS. We use X-FORWARDED-FOR headers, so if you whitelist Cloudflare IPs and deny all others, you’ll end up blocking visitor’s IPs instead.

Because the Cloudflare origin certificate doesn’t cover development environment hostnames, this only works on your default (production) environment. This means you will have to disable the feature on all development environments. Using HTTP access here is good, as we only want our developers to have access. If you don’t set it up, you’ll end up exposing your development origins, which is exactly what we want to protect here. So don’t forget to set it up.

Terminology and documentation

Now let’s understand what we’re going to use and set up before we actually do it.

Cloudflare uses the term origin pull, to describe their server sending a request to the origin you’ve configured, and using it accordingly. Basically, that’s just them using CURL and doing their thing with the response. The same process, but using mTLS, is called authenticated origin pull. Here’s Cloudflare documentation on how authenticated origin pull works. It’s thoroughly detailed and is absolutely worth reading.

On the Platform.sh side, we call it client authenticated TLS. It’s a popular method of protecting APIs in the mobile development world. They set this up so that only their app can make calls to their API. Of course, there are other use cases, and here we’ll use it to authenticate Cloudflare TLS certificates. Here’s the Platform.sh documentation on how to configure client authenticated TLS on your project.

Setup instructions

Be aware that those changes will cause downtime. I’d say you should plan for 2 hours of downtime. Meanwhile, your origin might not be available. If you don’t have one, you can set up a maintenance page in Cloudflare workers but that’s out of scope, so let’s move on.

Activate development mode

In your Cloudflare Dashboard for your site, go to Overview, and on the Quick Actions section, enable Development Mode. This will disable cache and it’ll cause Cloudflare to trigger origin pull on all your requests.

Of course, if you’ve set a maintenance page, you’ll also need to whitelist your IP or configure a way that you don’t see it. Workers can do that also.

Install Cloudflare origin certificates

The first thing we need to do is to install a Cloudflare origin certificate on your project.

Still in your site dashboard, click on SSL/TLS, and Origin Server. From there, create a certificate. Add your APEX domain, example.xyz, and a wildcard, *.example.xyz. If you have more than 3 labels add the specific domain you need. If you don’t know what a label is read Domain name - Wikipedia.

Once Cloudflare has generated the cert, download your certificate to your project root. Name the certificate cert.pem and the private key cert.key. Warning: you’ll get access to the private key only once. Afterward, you have to generate a new certificate. Save it in your password manager.

Now install the certificate at the origin with platform certificate:add --cert cert.pem --key cert.key. Ensure it’s properly installed with platform certificates. Once you see your certificate in the list, delete the certificate files.

You need to know that this disables automatic renewal of TLS certificates via Let’s Encrypt for your default environment. That’s important because Let’s Encrypt won’t be able to complete the validation challenge with mTLS in place.

At this point, Cloudflare should be able to communicate with Platform.sh using their certificate. Feel free to try browsing your site.

Activate Cloudflare Full TLS

For authenticated origin pull to work, Cloudflare needs to use Full SSL. Chances are, that when you turn it on, it’ll break your website. If that happens for you, read troubleshooting SSL errors.

Once you can get your site to work as expected. You can move to the next step.

Configure client authenticated TLS

Now we’re going to tell Platform.sh only to accept TLS enabled connections that use the Cloudflare CA cert.

First, download the Cloudflare CA cert. The one we need to use is listed under Zone-Level - Cloudflare certificate. Save the certificate in your project repository at .platform/cloudflare-ca.crt.

Now configure .platform/routes.yaml to enable the authentication as detail in the Platform.sh client authenticated TLS documentation.

For reference, here’s mine:

"https://{default}/":
  type: upstream
  upstream: "wordpress:http"
  tls:
    client_authentication: "require"
    client_certificate_authorities:
      - !include
        type: string
        path: cloudflare-ca.crt

Once you’re done, commit and push the changes.

When the deployment completes, go back to Cloudflare SSL/TLS Origin Server dashboard, activate Authenticated Origin Pulls.

The setup is now completed and your site should be working as expected.

Test the origin

At this point, we went through all this trouble to protect the origin, so let’s make sure it’s not answering us if we hit it directly.

Here’s how you can do that (in bash).

project_id='' #your project ID
default_env='' #your production environment name
origin_hostname=$(platform env:info edge_hostname -p ${project_id} -e ${default_env})
origin_ip=$(dig ${origin_hostname} +short|head -n1)
curl --insecure "https://${domain}/" --resolve ${domain}:443:${origin_ip}

This should throw an error or return no response. For once, that’s what we want.

And that’s it!

1 Like