Avoiding HTTP to HTTPS redirect loops in Varnish

Tags: http (2) vcl (20) tls (2)

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:

Redirect loop error message

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.

No native TLS support in Varnish Cache

The open source version of Varnish doesn’t have native TLS support:

  • Incoming TLS connections should be terminated by a TLS proxy like Hitch.
  • 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

Thanks to TLS proxy servers like Hitch you can terminate the TLS connection and communicate over plain HTTP to Varnish and from Varnish to the backend, as illustrated in the diagram below:

TLS termination diagram with Hitch

The goal is to enable HTTPS awareness by setting the URI scheme in the X-Forwarded-Proto header.

When a TLS connection is made, the following request header should be set:

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 with Hitch

Hitch is our preferred TLS termination solution. Because Hitch is a dedicated TLS proxy, it has no HTTP awareness and cannot set the X-Forwarded-Proto request header.

If you are using Hitch, you should connect to Varnish over the PROXY protocol.

Thanks to the PROXY protocol, we can use vmod_proxy in Varnish to extract the TLV attributes and know whether or not the connection was done over TLS.

This is the VCL code to set the X-Forwarded-Proto header:

vcl 4.1;

import proxy;

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

sub vcl_recv {
    if(!req.http.X-Forwarded-Proto) {
        if (proxy.is_ssl()) {
            set req.http.X-Forwarded-Proto = "https";
        } else {
            set req.http.X-Forwarded-Proto = "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 proxy;

backend default {
    .host = "127.0.0.1";
    .port = "8080";
}

sub vcl_recv {
    if(!req.http.X-Forwarded-Proto) {
        if (proxy.is_ssl()) {
            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";
    }
}