IP Restrictions behind Cloudflare and Varnish
I've recently been working with a client using Drupal, Varnish, and Cloudflare as part of their digital transformation journey. The client had requirements to ensure that requests coming in through Cloudflare, which should be all requests, would include a check to ensure only their internal IP ranges and ours would be able to access administration pages on the site.
Looking at the Cloudflare interface, I can see that while there are white and black lists, none were able to be based on paths. So let's allow Varnish to come to the rescue.
Initially, I tried using the client.ip variable provided by Varnish to check against acls, but upon further investigation when it didn't work (including a deep dive into the Varnish source), I realised that Varnish was using the IP address garnered from the socket connection made to the server. With Cloudflare sitting between the user and Varnish, Varnish was seeing the client.ip as Cloudflare not the user.
From here, I attempted to compare the CF-Connecting-IP header sent by Cloudflare to the acls defined. Varnish didn't like this one bit as all headers are defined as strings whereas the client.ip (and what acls expect) is a special structure dissimilar to a string.
Even further investigation revealed that Varnish 4's included vmod 'std' includes a method which converts strings to this special IP structure. Putting all of these things together, we come up with the following snippet to block access to /user and /admin for users not accessing via the defined IPs.
Finally, and most importantly, remember to define your acls at the very top of your vcl; especially if you're concatenating multiple vcl files.
acl deloitte {
"1.2.3.4";
"2.3.4.5";
}
acl client {
"5.6.7.8";
"6.7.8.9";
}
sub vcl_recv {
# Block access for the administrative part of the site for users not in Deloitte or client.
if (req.http.CF-Connecting-IP) {
if ((req.url ~ "^/user" || req.url ~ "^/admin") && !(std.ip(req.http.CF-Connecting-IP, "0.0.0.0") ~ deloitte || std.ip(req.http.CF-Connecting-IP, "0.0.0.0") ~ client)) {
return (synth(403, "Forbidden"));
}
}
}
sub vcl_synth {
if (resp.status == 403) {
set resp.http.Content-Type = "text/html; charset=utf-8";
synthetic( {"<!DOCTYPE html>
<html>
<head>
<title>"} + resp.status + " " + resp.reason + {"</title>
</head>
<body>
<p>Error "} + resp.status + " " + resp.reason + {"</p>
</body>
</html>
"} );
return (deliver);
}
}