Varnish and HTTPS with Apache

Today I wanted to run a website with HTTPS and Varnish. Varnish itself does not support SSL, and for good reasons. So the setup we need is HTTPS termination => Varnish => Webserver. So we need a software that can handle HTTPS and forward requests over HTTP to Varnish. A couple of options for HTTPS termination are HAProxy, Nginx, Squid or Apache. I went with Apache, as it was already installed on the server in question, thus reducing the complexity of the setup.

On Apache, you need to enable mod_proxy. The Apache SSL configuration looks like this:

#/etc/apache2/sites-available/example.com-ssl
<VirtualHost *:443>
    ServerName www.example.com

    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:80/
    RequestHeader set X-Forwarded-Port "443"
    RequestHeader set X-Forwarded-Proto "https"

    SSLEngine On
    SSLCertificateFile /etc/apache2/ssl/example.com.crt
    SSLCertificateKeyFile /etc/apache2/ssl/example.com.key
    SSLCertificateChainFile /etc/apache2/ssl/example.com.chain
</VirtualHost>

Note the extra headers I set. These are useful to let the application behind the proxy know that it was requested through an SSL terminator and should generate absolute URLs with https. This system is used for example by Symfony.

127.0.0.1:80 is the location where the Varnish server is running. Varnish in turn is configured to talk to the HTTP server for the actual content. In our case, this is Apache again, with a completely standard configuration, except that I moved the port to 8000.

One more bit we want is to redirect external requests that go directly to HTTP to HTTPS. This happens for example when users type our domain into a browser without specifying the protocol. Not responding on port 80 would be bad. So what we do is redirect if the request did not go through the proxy, inspired by a varnish wiki entry:

# /etc/varnish/default.vcl
acl local {
    "127.0.0.1"
}

sub vcl_recv {
    # ensure https
    if (client.ip !~ local) {
        set req.http.x-Redir-Url = "https://www.example.com" + req.url;
        error 750 req.http.x-Redir-Url;
    }

    # ...
}

sub vcl_error {
    # redirect
    if (obj.status == 750) {
        set obj.http.Location = obj.response;
        set obj.status = 302;
        return (deliver);
    }
}

Note that in Varnish 4, the name of the error method was changed to syntetic which makes more sense.

Bonus: Protect Stage by IP or Basic Authentication

We also need a server stage.example.com. We want this server to be freely accessible from our network, but from outside, users must provide a basic authentication password. Apache allows us to do this. There are super complicated documentations on how to nest Require statements to be found in the Apache documentation. But actually, we can do much simpler, as explained here in the Apache wiki:

<VirtualHost *:443>
    ServerName stage.example.com

    <PROXY *>
        AuthUserFile /etc/apache2/stage-htpasswd
        AuthType Basic
        AuthName Example
        Require valid-user
        Order allow,deny

        # My static IP
        Allow from 1.2.3.4

        Satisfy any
    </PROXY>

    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:80/
    RequestHeader set X-Forwarded-Port "443"
    RequestHeader set X-Forwarded-Proto "https"

    SSLEngine On
    SSLCertificateFile /etc/apache2/ssl/example.com.crt
    SSLCertificateKeyFile /etc/apache2/ssl/example.com.key
    SSLCertificateChainFile /etc/apache2/ssl/example.com.chain
    SSLProtocol All -SSLv2 -SSLv3
</VirtualHost>

The important bit here is the Satisfy any.

Now you should test this both from your local network and from remote locations to make sure the password is really required (e.g. using your mobile phone, or ssh into an external server).