Skip to main content

Routing your domain securely via Cloudflare

In this lesson you'll learn how to fully integrate Cloudflare with your Nginx server. We'll cover how to block direct access to your origin, enforce mutual TLS with Cloudflare using Authenticated Origin Pulls, replace self-signed certificates with Cloudflare-issued origin certificates, and configure Nginx to log the real visitor IPs.

Even though we currently have a domain set up on Cloudflare, our current configuration allows anyone on the Internet to access our HTTPS service directly, either by using the IP address of our server, or by guessing our origin host name.

This presents several challenges, primarily related to security and performance. For example, direct visitors will bypass any WAF (web application firewall) and caching rules configured on Cloudflare, including rate limiting and captcha challenges.

Allowing only Cloudflare traffic

There are multiple ways to lock our server down, to accept HTTPS traffic from Cloudflare only. For example, we can use the UFW firewall to block all HTTPS traffic except traffic coming from Cloudflare's IPs. While this is a viable option, it does require maintaining lists of IPs.

Another way is using Cloudflare's AOP (Authenticated Origin Pulls). This asks Cloudflare to present a client certificate while communicating with the origin server. If we can verify this certificate, we can be sure that the request is coming from Cloudflare.

Let's enable this option for our domain in the Cloudflare Dashboard under the SSL/TLS - Origin Server section:

Cloudflare AOP

However, simply enabling it is not enough. We have to validate this certificate in Nginx.

Nginx configuration

First let's download the Cloudflare AOP certificate and add it to our configs repository:

cd /config/ssl
sudo curl -o cloudflare-aop.pem https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem

Next, let's update our /config/nginx/uncached.org.conf file to verify the client's request against this certificate inside our server block:

ssl_client_certificate /config/ssl/cloudflare-aop.pem;
ssl_verify_client on;

Save and reload your Nginx configuration:

sudo systemctl reload nginx.service

At this point, we should no longer be able to access our HTTPS service without presenting a client certificate. We can verify this using cURL locally:

curl -v --insecure -H "Host: uncached.org" https://127.0.0.1/test.php

This should now respond with a 400 error code, and a "No required SSL certificate was sent" message.

Nginx client certificate error

This is because we have not sent a client certificate at all. We can also make sure that the Cloudflare certificate is actually validated, by sending a request with a different client certificate instead.

If you still have your self-signed certificate and key lying around, you can test with those:

sudo curl \
    --cert /config/ssl/uncached.org.cert.pem \
    --key /config/ssl/uncached.org.key.pem \
    -v --insecure -H "Host: uncached.org" \
    https://127.0.0.1/test.php

This will send the wrong client certificate to Nginx, and it should respond with code 400 and a "SSL certificate error". Viewing the domain through Cloudflare (in a browser for example) should still work just fine:

Cloudflare AOP browser

Now that we have a way to fully trust requests coming from Cloudflare, let's also make sure Cloudflare can fully trust responses coming from our server.

Origin certificates

Previously when we set up our first site in Nginx, we generated a self-signed certificate to serve over HTTPS. Let's now replace that certificate with one generated by and trusted by Cloudflare:

Cloudflare Origin Certificate

You can generate a new certificate in the SSL/TLS - Origin Server - Origin Certificates - Create Certificate section in your Cloudflare dashboard. You'll be able to select the host names to add to the certificate too. I've added uncached.org and a wildcard *.uncached.org to mine, which covers www.uncached.org and other subdomains I may wish to add in the future.

Copy the resulting Origin Certificate and Private Key to your server configs, replacing the old self-signed ones. Mine are in /config/ssl/uncached.org.cert.pem and /config/ssl/uncached.org.key.pem respectively. Note that the permissions on the private key file should be 0600, owned by root:

Cloudflare cert permissions

You can double-check your certificate contents (issuer, dates, etc.) with openssl:

openssl x509 -in uncached.org.cert.pem -noout -text

Then reload your Nginx service using:

sudo systemctl reload nginx.service

Now you should be able to update your Cloudflare SSL/TLS settings and set the encryption mode to Full (Strict). This option enforces validation of the newly generated origin certificates.

Cloudflare SSL Full Strict

If you've done everything correctly, you should still be able to access your site via Cloudflare from a browser window.

HTTP to HTTPS redirect

Our Nginx server doesn't even listen on the insecure HTTP port, however with the default settings, Cloudflare will still attempt to send traffic to port 80 if the user requested an HTTP resource.

Cloudflare HTTPS redirect

We can address this by redirecting all HTTP traffic to HTTPS at the edge. You can find this setting in SSL/TLS - Edge Certificates - Always Use HTTPS in the Cloudflare dashboard. You can optionally turn on HSTS there too and configure your TLS versions if needed.

Nginx Real-IP

Being behind Cloudflare, there's one last thing we need to resolve: visitors' real IPs. If you look at your /var/log/nginx/access.log you will see traffic coming from certain IP addresses. You can use whois to get more information on those IPs and you'll soon find that all of them belong to Cloudflare.

This is not surprising, because all this traffic is indeed coming from Cloudflare. However, this will be a problem for any application security we implement going forward, including any audit and logging. We'll want the real end user's IP addresses for those purposes.

This is where Nginx's realip_module comes in. It allows us to set the client's IP address from a header, which is propagated down to logs, FastCGI (and PHP) requests and more.

Cloudflare sends us the visitor's IP address in a CF-Connecting-IP header. Let's update our site configuration (mine is in /config/nginx/uncached.org.conf) to include this in the server block:

set_real_ip_from 0.0.0.0/0;
real_ip_header CF-Connecting-IP;

The first directive tells Nginx that we can trust the real IP header from any source. We can only do this because we know that the only source for our traffic in this server block is Cloudflare, due to the AOP configuration we implemented earlier. Do not do this if you are not enforcing AOP, otherwise you allow anyone to spoof IPs.

The next directive tells Nginx which header to derive the IP address from. After saving your configuration and reloading your Nginx service, you should now see your ISP's or VPN IP address in the access log.

Reusable configuration

If you're planning to run multiple WordPress sites from the same dedicated or virtual server, you'll find yourself copying chunks of configuration for every site you configure. I encourage you to find reusable things in the configuration, especially in Nginx, and provide these as snippets you could include instead.

For example, the client certificate, client certificate verification and the realip configuration for Cloudflare are not going to change, so you could move them to a /config/nginx/cloudflare.conf and include that file inside the server block to make it a bit cleaner.

This will also prevent you from making a mistake in a future configuration, by leaving out ssl_verify_client for example, allowing non-Cloudflare traffic through.

Enroll
Enjoying the course content? Enroll today to keep track of your progress, access premium lessons and more.