Avoiding HTTP to HTTPS redirect loops in Varnish
When your force HTTP to HTTPS redirection in your web server or web application and cache the output, you might get stuck in a redirect loop. Your browser may present the following error message as a result:

Behind the scenes, your browser will receive a 301 Moved Permanently status code and your browser will follow the URL from the Location header:
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
Content-Length: 226
Content-Type: text/html; charset=iso-8859-1
X-Varnish: 2
Age: 0
Via: 1.1 varnish
Despite using an https:// URI scheme, the origin server will continue to issue redirects and you’re in fact stuck in a redirect loop. This loop continues until your browser gives up, at which point the error message will appear.
This tutorial assumes that TLS is not enabled in Varnish. Prior to the release of version 9, Varnish did not support native TLS and required a TLS proxy to offload TLS.
We advise against this solution and recommend you use Varnish’s native TLS capabilities to avoid this issue.
No native TLS support in older versions of Varnish
Old versions of Varnish didn’t support native TLS and required a TLS proxy like Hitch. Meanwhile backend connections to the origin server only support plain HTTP.
This means that your web server or web application only receives plain HTTP requests, regardless of the protocol of the incoming client connection. Even if you terminate an incoming TLS connection, the web application will always keep enforcing HTTPS through a redirection.
HTTPS awareness through the X-Forwarded-Proto header
When terminating TLS and communicating with Varnish over plain HTTP, the goal is to enable HTTPS awareness by setting the URI scheme in the X-Forwarded-Proto header:
X-Forwarded-Proto: https
This will allow the origin server to know what the forwarded protocol was and whether or not a redirection should be issued.
Setting the X-Forwarded-Proto header in Varnish
If you’re using a pure TLS proxy without HTTP awareness, you have to set the X-Forward-Proto header in Varnish.
This is the VCL code to set the X-Forwarded-Proto header:
import std;
sub vcl_recv {
if (!req.http.X-Forwarded-Proto) {
if(std.port(server.ip) == 8443) {
set req.http.X-Forwarded-Proto = "https";
} else {
set req.http.X-Forwarded-Proto = "http";
}
}
}
The code assumes that there is a TLS proxy that handles traffic on port 443 and proxies to TLS-terminated traffic on port 8443. All traffic on this port is HTTPS traffic, resulting in the X-Forward-Proto header being set to https. In all other cases, the traffic is plain HTTP, resulting in X-Forwarded-Proto being set to http.
Setting the X-Forwarded-Proto header in HAProxy
If you are using HAProxy as your TLS proxy, you can use the http-request set-header directive to set the X-Forwarded-Proto header to https:
http-request set-header X-Forwarded-Proto https
Setting the X-Forwarded-Proto header in Apache
If you are using Apache as a TLS proxy, you can use the RequestHeader set directive to set the X-Forwarded-Proto header to https:
RequestHeader set X-Forwarded-Proto "http"
Setting the X-Forwarded-Proto header in Nginx
If you are using Nginx as a TLS proxy, you can use the proxy_set_header directive to set the X-Forwarded-Proto header to https:
proxy_set_header X-Forwarded-Proto https;
No standard HTTPS awareness in Varnish
As described in the built-in VCL, Varnish identifies a cached object by a hash that is created using the request URL and the Host header.
The request URL doesn’t contain the URI scheme, which means by default Varnish has no HTTPS awareness.
By default, Varnish considers the 301 status code to be cacheable and because the origin server continuously issues redirects, the redirection will be cached.
So despite the X-Forwarded-Proto header being sent, Varnish will still only cache one version. Whether the HTTP version or the HTTPS version is cached depends on what protocol is used for the first request.
- If the HTTP version is requested first, the redirect will be cached and you will still end up in a redirect loop
- If the HTTPS version is cached, plain HTTP requests will return content with HTTPS references which will result in mixed content warnings in your browser
Create cache variations based on the X-Forwarded-Proto header
Enabling HTTPS awareness in Varnish can be done by creating cache variations for every cached object based on the X-Forwarded-Proto header.
This can be done by return the following HTTP response header:
Vary: X-Forwarded-Proto
Varnish will process this header and will use the value of the X-Forwarded-Proto request header to extend the hash. This means there will be a cache variation for the HTTP version and a cache variation for the HTTPS version.
You can set this header in your application code, but you can also set it in VCL:
vcl 4.1;
import std;
backend default {
.host = "127.0.0.1";
.port = "8080";
}
sub vcl_recv {
if (!req.http.X-Forwarded-Proto) {
if(std.port(server.ip) == 8443) {
set req.http.X-Forwarded-Proto = "https";
} else {
set req.http.X-Forwarded-Proto = "http";
}
}
}
sub vcl_backend_response {
if(beresp.http.Vary) {
set beresp.http.Vary = beresp.http.Vary + ", X-Forwarded-Proto";
} else {
set beresp.http.Vary = "X-Forwarded-Proto";
}
}
The real solution: use native TLS in Varnish
As mentioned in the beginning of this tutorial: as of Varnish version 9 there is native TLS support built in. Of course Varnish Enterprise has had native TLS support for years.
If you configured TLS in Varnish, incoming connections over HTTPS will not be a problem. If you want to send HTTPS requests to your web server, simply add .ssl = 1; to your backend definition as seen below:
backend default {
.host = "127.0.0.1";
.port = "443";
.ssl = 1;
}
HTTP to HTTPS redirection
To avoid that plain HTTP requests are proxied to the origin web server as HTTPS requests, we can perform HTTP to HTTPS redirection in Varnish. The following VCL code would capture plain HTTP requests and redirect the browser to the HTTPS equivalent:
import std;
backend default {
.host = "127.0.0.1";
.port = "443";
.ssl = 1;
}
sub vcl_recv {
unset req.http.location;
if (std.port(server.ip) != 443) {
set req.http.location = "https://" + req.http.host + req.url;
return (synth (301));
}
}
sub vcl_synth {
if (resp.status == 301 ||
resp.status == 302 ||
resp.status == 303 ||
resp.status == 307) {
if (!req.http.location) {
std.log("location not specified");
set resp.status = 503;
return (deliver);
} else {
set resp.http.location = req.http.location;
return (deliver);
}
}
}
Proxy over plain HTTP and still use X-Forwarded-Proto
Despite accepting HTTPS requests directly in Varnish, you can still choose to communicate with your origin web server over plain HTTP. In that case the X-Forwarded-Proto header is still required to signal the original protocol that was used for the request.
This is what the VCL code could look like:
vcl 4.1;
import std;
backend default {
.host = "127.0.0.1";
.port = "8080";
}
sub vcl_recv {
if (!req.http.X-Forwarded-Proto) {
if(std.port(server.ip) == 443) {
set req.http.X-Forwarded-Proto = "https";
} else {
set req.http.X-Forwarded-Proto = "http";
}
}
}
sub vcl_backend_response {
if(beresp.http.Vary) {
set beresp.http.Vary = beresp.http.Vary + ", X-Forwarded-Proto";
} else {
set beresp.http.Vary = "X-Forwarded-Proto";
}
}
Varnish also comes with a TLS VMOD that can detect TLS connections without checking the port. This leverages the tls.is_tls() function, and this is what that VCL code looks like:
vcl 4.1;
import tls;
backend default {
.host = "127.0.0.1";
.port = "8080";
}
sub vcl_recv {
if (!req.http.X-Forwarded-Proto) {
if(tls.is_tls()) {
set req.http.X-Forwarded-Proto = "https";
} else {
set req.http.X-Forwarded-Proto = "http";
}
}
}
sub vcl_backend_response {
if(beresp.http.Vary) {
set beresp.http.Vary = beresp.http.Vary + ", X-Forwarded-Proto";
} else {
set beresp.http.Vary = "X-Forwarded-Proto";
}
}