Putting an nginx proxy behind Cloudflare

Why bother?

Since this is my home lab and it’s running on my home connection, I definitely prefer to cut down on the number of people able to poke at things. The first layer of defense is obviously a firewall (with a whitelist!) to only allow access to select services, i.e., the VPN and emergency SSH, but what about services that are intended for the public like the nginx server? That’s where a reverse proxy comes in. There are many reasons that you’d want to keep your site behind a reverse proxy: Internet scumbags, whitehats who scan the internet and then sell information on your open ports and services, DDoS protection, etc. In this case, it’s going to add a layer of obfuscation to my origin address.

How Cloudflare Works…and mediocre ASCII art diagrams

Cloudflare provides a reverse proxy–and various other security features–much like the nginx proxy that we’ve already set up. The difference is that their network can handle DDoS and do helpful things like serve HTTP sites over HTTPS. You point your DNS to their servers and they transparently proxy traffic to you.

Normally:
[ Alice ] <-> [ Your web server with public IP address ]

  1. Alice sends a DNS request for geek.cm.
  2. DNS resolves geek.cm to 1.2.3.4.
  3. Alice requests http://1.2.3.4:80 with Host: geek.cm
  4. Web server returns the content to Alice.

With Cloudflare (or similar reverse proxy service):
[ Alice ] <-> [ Cloudflare ] <-> [ Your web server ]

  1. Alice sends a DNS request for geek.cm.
  2. DNS resolves geek.cm to one of Cloudflare’s servers.
  3. Alice requests http://cloudflare_ip:80 with Host: geek.cm
  4. Cloudflare’s servers request http://1.2.3.4:80 with Host: geek.cm
  5. Web server returns the content to Cloudflare.
  6. Cloudflare returns the content to Alice.

The difference is that Alice sees a Cloudflare address instead of yours, thus hiding your origin address. There’s some other stuff Cloudflare can do like serve as a web application firewall, upgrade requests to HTTPS, and so on, but we’re focusing on the core functionality–protecting our home network from the internet.

Translating requestor IP addresses

If you’re familiar with running a web server, you’re probably asking yourself, “But if Cloudflare is requesting all of the pages, then aren’t my logs full of Cloudflare’s IP address? What about my analytics?” or “How do I know who’s sending all of these LFI/RFI/SQLi requests?” Fortunately, Cloudflare documents this process[1] and it’s basically a cut-and-paste job.[2] I’ve removed the IPv6 addresses because I don’t allow IPv6 requests past my firewall.

server {
    ...
    set_real_ip_from 103.21.244.0/22;
    set_real_ip_from 103.22.200.0/22;
    set_real_ip_from 103.31.4.0/22;
    set_real_ip_from 104.16.0.0/12;
    set_real_ip_from 108.162.192.0/18;
    set_real_ip_from 131.0.72.0/22;
    set_real_ip_from 141.101.64.0/18;
    set_real_ip_from 162.158.0.0/15;
    set_real_ip_from 172.64.0.0/13;
    set_real_ip_from 173.245.48.0/20;
    set_real_ip_from 188.114.96.0/20;
    set_real_ip_from 190.93.240.0/20;
    set_real_ip_from 197.234.240.0/22;
    set_real_ip_from 198.41.128.0/17;

    real_ip_header CF-Connecting-IP;
    ...
}

The set_real_ip_from lines indicate servers that we trust to send the real client IP address. The real_ip_header line will read the header CF-Connecting-IP to any request coming from Cloudflare and set the client address to the value contained in that header. Now our nginx logs show the real IP address of requests instead of Cloudflare’s servers.

Locking down nginx for Cloudflare

When you’re configuring a web service for security behind some sort of proxy (e.g., Cloudflare), you should always restrict the incoming connections at the firewall. There are countless sites that put up Cloudflare and expect that no one will be able to find their origin address. It’s certainly not easy to track down a misconfigured site behind Cloudflare, but it can be done, especially if the attacker is only looking for one or two domains. A simple brute force of the IPv4 space making requests with the appropriate Host header to each IP address will eventually reveal the origin address. Thus, it’s important to have a whitelist in place that only allows traffic from Cloudflare or other trusted hosts.

# firewall-cmd --permanent --new-ipset=cf --type=hash:net
# for cidr in $(curl https://www.cloudflare.com/ips-v4); do \
>     firewall-cmd --permanent --ipset=cf --add-entry=$cidr; \
> done

# firewall-cmd --permanent --add-rich-rule='rule source ipset=cf port port=80 protocol=tcp accept'
# firewall-cmd --permanent --add-rich-rule='rule source ipset=cf port port=443 protocol=tcp accept'
# firewall-cmd --permanent --add-rich-rule='rule source ipset=cf invert="True" port port=80 protocol=tcp drop'
# firewall-cmd --permanent --add-rich-rule='rule source ipset=cf invert="True" port port=443 protocol=tcp drop'
# firewall-cmd --reload

Although it’s rare, Cloudflare’s IP addresses can change, so having a daily cron job like the following may be useful:

#!/bin/bash
firewall-cmd --permanent --delete-ipset=cf
firewall-cmd --permanent --new-ipset=cf --type=hash:net
for cidr in $(curl https://www.cloudflare.com/ips-v4); do \
    firewall-cmd --permanent --ipset=cf --add-entry=$cidr; \
done
firewall-cmd --reload

With these rules in place, we don’t have to worry about ending up on Shodan or Censys since any traffic that doesn’t originate from Cloudflare’s reverse proxies will be dropped. The cron job ensures that if Cloudflare adds more reverse proxies or changes their IP ranges, we aren’t denying that traffic.

Configuring Cloudflare

Since we’re using Cloudflare, arguably we don’t even need a LetsEncrypt cert since Cloudflare can proxy HTTPS to an HTTP backend and they’ll issue a SAN cert for your domain. This is OK for testing, but not really acceptable for anything that requires any security because even though the end user’s connection to Cloudflare is encrypted, Cloudflare’s connection to your origin is still HTTP and that means plaintext. Ideally, you want the traffic encrypted between both connections–the end user to Cloudflare and Cloudflare to you. To do this, you can enable the Full SSL option which proxies HTTPS to HTTPS. Cloudflare will ignore self-signed certs, so your visitors see “the green lock” and you get end-to-end encrypted traffic. However, the best option is Full (Strict) SSL mode where Cloudflare requires a valid certificate on your origin.

Why does it matter if the cert is valid if everything’s still encrypted? I don’t know. I can’t think of a threat model where an attacker is stopped by Full vs. Full (Strict). However, testing and internal access work a lot more smoothly if you need to go around Cloudflare and not have your browser complain.

Update (2018-01-08): After talking to a friend at Cloudflare, there is a scenario where Full (Strict) could be valuable: If you already have a valid certificate for your domain and you enable Cloudflare’s Always use HTTPS option. If you allow HTTP, then someone MITMing the connection between Cloudflare and your server could request a valid certificate for your domain and successfully sit behind Cloudflare’s Full SSL mode. However, with Always use HTTPS and Full (Strict), Cloudflare will require a valid cert from the origin which presumably the MITM doesn’t have, so they can’t receive unencrypted requests, can’t request a certificate, and can’t MITM the traffic.


Footnotes

[1] https://support.cloudflare.com/hc/en-us/articles/200170706-How-do-I-restore-original-visitor-IP-with-Nginx-

[2] Note that these are the ranges from https://www.cloudflare.com/ips-v4

Leave a Reply

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