Creating a reverse proxy to a multi-site server with Caddy
You need to add Host headers and HTTPS configuration to your reverse_proxy
block.
I was setting up Caddy to run as a reverse proxy in front of the www.flickr.org
WordPress site, which is currently hosted on WordPress VIP. Here’s the config I needed:
reverse_proxy https://192.0.66.95 {
transport http {
tls_server_name "go-vip.co"
}
header_up Host "www.flickr.org"
}
It took a while to work out this config, so I want to write down my debugging steps and some useful links.
Context
- At time of writing, our WordPress VIP server runs at
192.0.66.95
- Following my own advice, I tested the migration with the alternative domain
www2.flickr.org
How I got here
-
I started with a one-line configuration of the
reverse_proxy
directive:www2.flickr.org { reverse_proxy https://192.0.66.95 }
This fails with an HTTP 502 Bad Gateway error.
I realised that this is probably because
192.0.66.95
is serving many sites, and it doesn’t know it should be servingwww.flickr.org
to the proxy. -
I found a section in the
reverse_proxy
docs that suggested adding aHost
header:Since (most) headers retain their original value when being proxied, it is often necessary to override the Host header with the configured upstream address when proxying to HTTPS, such that the Host header matches the TLS ServerName value:
reverse_proxy https://example.com {
header_up Host {upstream_hostport}
}
I’m not sure what
{upstream_hostport}
does, so I decided to hard-code the header instead – I know what it’s going to be:www2.flickr.org { reverse_proxy https://192.0.66.95 { header_up Host "www.flickr.org" } }
This fails with another HTTP 502 Bad Gateway error.
-
I wasn’t sure what was failing this time, so I enabled debug logs in Caddy):
{ debug }
Then I could see all the requests being sent to the upstream proxy, including this one:
2024/11/21 11:58:25.072 DEBUG http.handlers.reverse_proxy upstream roundtrip { "upstream": "192.0.66.95:443", "duration": 0.004109724, "request": { "proto": "HTTP/2.0", "method": "GET", "host": "www.flickr.org", "uri": "/", "headers": { … }, "tls": { … }, … }, "error": "tls: failed to verify certificate: x509: cannot validate certificate for 192.0.66.95 because it doesn't contain any IP SANs" }
That last message is interesting!
tls: failed to verify certificate: x509: cannot validate certificate for 192.0.66.95 because it doesn't contain any IP SANs
I’m not sure what an IP SAN is, but it's a clue!
-
I tried making an HTTPS request to that IP address using curl:
$ curl -v 'https://192.0.66.95' * Trying 192.0.66.95:443... * Connected to 192.0.66.95 (192.0.66.95) port 443 … * SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF * Server certificate: * subject: CN=go-vip.co * start date: Nov 19 09:11:52 2024 GMT * expire date: Feb 17 09:11:51 2025 GMT * subjectAltName does not match ipv4 address 192.0.66.95 * SSL: no alternative certificate subject name matches target ipv4 address '192.0.66.95' * Closing connection curl: (60) SSL: no alternative certificate subject name matches target ipv4 address '192.0.66.95' More details here: https://curl.se/docs/sslcerts.html curl failed to verify the legitimacy of the server and therefore could not establish a secure connection to it. To learn more about this situation and how to fix it, please visit the web page mentioned above.
So now I know that “IP SAN” stands for “IP Subject Alternative Name”.
-
I’ve lost the link, but I saw somebody suggest looking at the
http
transport settings for thereverse_proxy
directive.This led me to the
tls_server_name
directive, which tells Caddy what the name of the server is – and presumably the name it should look for on the HTTPS certificate.I could see from the
curl
output that the server name isgo.vip.co
, so I added those lines to the config:www2.flickr.org { reverse_proxy https://192.0.66.95 { header_up Host "www.flickr.org" transport http { tls_server_name go-vip.co } } }