Proxy subdomains of a remote domain with nginx

I have an old Windows virtual machine image that I run for a couple of legacy applications. I can’t remember now whether it’s so old that TLS can’t be upgraded to support modern browsers, but I think that’s why I can’t have https connections to it. Yes, I still need to access it occasionally. Don’t ask.

I also use 1Password — an excellent password manager, but also quite handy for storing test identities for prefilling form fields. But it whinges when asked to prefill a form on a page without https. Queue task to give that old Windows VM some https love via proxy!

Configuring nginx as a generic proxy

Configuring nginx for https is well described in the nginx documentation, so I’m not going to repeat that here. You can read that documentation, or find a blog post on that somewhere if the doco doesn’t help you.

I have quite a number of domains hosted on that Windows VM, so rather than spec some proxy rules for each one, here’s how to handle that generically. The rules below map a subdomain of the proxy server to a domain with that name on the real server, like this:

broccoli.ancientproxy.local ➡️ broccoli.local

To do that, we need to capture the subdomain part of the proxy domain name and reuse it later in the config. A nice, easy way to do that is with an nginx map directive that matches with a regular expression.

Notice the default value of the map: it’s the name of the remote server, which means that the proxy works both for subdomains mapped to domains hosted on that server, and for the remote server’s own domain name accessed through the proxy without a subdomain.

After setting up the map, we need to set up the server block that listens to all subdomains of the proxy domain, on both IPv4 and IPv6. For any request to those domains, we need to pass (proxy) the request along to the remote server with a Host header for the target domain — e.g. broccoli.local — so that the web server knows what site was originally requested. We also need to pass along some information so that any apps running on that remote server can tell that the connection was https and where it originally came from (i.e. not just the proxy server).

# extract the subdomain from the requested domain name
map $host $ancient_subdomain {
    default "ancientvm.";
    ~(?<x_ancient_subdomain>.*\.)ancientproxy\.local $x_ancient_subdomain;
}

server {

    # the dot at the start allows subdomains to be matched
    server_name .ancientproxy.local;

    # listen to https on both IPv4 and IPv6, port 443
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    location / {
        proxy_pass http://ancientvm.local/;

        # tell the remote server what website we want
        proxy_set_header Host "${ancient_subdomain}local";

        # tell remote applications about https
        proxy_set_header X-Forwarded-Proto $scheme;

        # tell remote applications where the request really came from
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

}

And job is done, with an old VM connected to the modern TLS world.