Using CloudFlare for its firewall capabilities has undeniable benefits, but it’s not as foolproof and transparent as one may think. In this post I’ll explain how to configure nginx on your server for better integration and security.
Prior experience with CloudFlare and some knowledge of nginx configuration is assumed.
Obtaining the visitor IP
When you set CloudFlare to act as a proxy, the remote address becomes one of CloudFlare’s IPs and it gets everywhere – in log files and applications (e.g PHP's $_SERVER['REMOTE_ADDR'
]). The original address is placed in two headers, CF-Connecting-IP
and X-Forwarded-For
. The difference between the two is that CF-Connecting-IP
keeps the original client IP, while X-Forwarded-FOR
keeps a whole chain of IPs if there are multiple proxies.
For a while I relied on a small PHP function to get the original IP:
function proxy_safe_get_ip() { $ip_keys = [ 'HTTP_CF_CONNECTING_IP', 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'HTTP_X_FORWARDED_IP', 'HTTP_X_REAL_IP', 'REMOTE_ADDR' ]; foreach ($ip_keys as $key) { if (array_key_exists($key, $_SERVER)) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE|FILTER_FLAG_NO_RES_RANGE)) return $ip; } } } return ''; }
I intended to make this function work with all sorts of proxies, not just CloudFlare. The function looks for several headers, in order of preference. For each header, it takes the value and, if needed, splits it by a comma (useful for HTTP_X_FORWARDED_FOR
) and gets the first valid IP. The last header to try is REMOTE_ADDR
, so it defaults to that if nothing else is found.
This function served me well over the years, but you don’t always have control over the application.
Fortunately nginx has a very clever feature. You can designate a list of IPs as trusted proxies and nginx will write back the original IP for you.
So you can simply write in any http, server or location block:
set_real_ip_from xxx.xxx.xxx.xxx; real_ip_header CF-Connecting-IP;
Disallowing direct connections
Just because CloudFlare acts as a proxy (and WAF) doesn’t mean your server IP is protected. An attacker could learn the origin IP in many ways. For example, Censys keeps a history of SSL certificates for domains and the IPs they were used for. It would make sense therefore to only allow connections from CloudFlare servers and disallow everything else. This is easily accomplished with:
allow xxx.xxx.xxx.xxx; deny all;
There’s a problem here. If you also use the previous feature to get back to visitor IP, you no longer have the CloudFlare IPs but the real visitor IPs, so you can’t use allow/deny.
I spent quite some time on this issue. The solutions on the Internet were incomplete or, in some cases, plain wrong. After asking around and failing to find a single correct answer, I re-read the docs and saw that nginx places the original (proxy) IP in a variable called $realip_remote_addr
.
The final piece of the puzzle is a rather obscure geo directive. It can be placed only in a http
block and it can set a variable based on another on another one. It defaults to $remote_addr
but other variables can be used.
Armed with this knowledge, we can write:
geo $realip_remote_addr $isCloudFlare { xxx.xxx.xxx.xxx 1; yyy.yyy.yyy.yyy 1; default 0; }
Then, wherever you want to block connections not coming from CloudFlare, you’d write:
if ($isCloudFlare = 0) { return 403; }
Getting the list of CloudFlare IP and keeping them up-to-date
CloudFlare doesn’t seem to change the IPs very often, but it’s still a good practice to keep this list updated. You can manually get the list and include it, but we can do better:
#!/bin/bash CF_REALIP=/etc/nginx/snippets/cf_realip.conf CF_ALLOWIP=/etc/nginx/snippets/cf_allowdeny.conf CF_GEO=/etc/nginx/snippets/cf_geo.conf echo "#Cloudflare" > $CF_REALIP; echo "#Cloudflare" > $CF_ALLOWIP; echo "#Cloudflare" > $CF_GEO; echo "default 0;" >> $CF_GEO; for i in `curl https://www.cloudflare.com/ips-v4`; do echo "set_real_ip_from $i;" >> $CF_REALIP; echo "allow $i;" >> $CF_ALLOWIP; echo "$i 1;" >> $CF_GEO; done for i in `curl https://www.cloudflare.com/ips-v6`; do echo "set_real_ip_from $i;" >> $CF_REALIP; echo "allow $i;" >> $CF_ALLOWIP; echo "$i 1;" >> $CF_GEO; done echo "real_ip_header CF-Connecting-IP;" >> $CF_REALIP; echo "allow 127.0.0.1;" >> $CF_ALLOWIP; echo "allow ::1;" >> $CF_ALLOWIP; echo "deny all;" >> $CF_ALLOWIP; nginx -t && systemctl reload nginx
This script downloads the latest lists of IPv4 and IPv6 CloudFlare addresses and writes 3 config files for nginx in /etc/nginx/snippets
: One for real_ip
, one allow/deny and one for the geo
directive. You can then include those files where you need them. Of course, you can also set up a cron job to run from time to time to update the IPs automatically.
Authenticated Origin Pull
While not directly related to the article, there is another way of disallowing direct connections and ensuring all connections to the server come from CloudFlare. Authenticated Origin Pull uses a more complex certificate-based approach than an IP whilelist and should be more secure but also more difficult.