Nginx selective TLS passthrough reverse proxy based on SNI

nginxreverse-proxysnissl

I have system of IoT devices behind a NAT, so they are not accessible from the public internet (although it's desired).
To overcome this I tied them into a VPN, with one member exposed to the public internet to act as a gateway.
The VPN has an internal domain set up, and each member of the network has a subdomain based on a unique ID (let's go with MAC address), like so: 12a4f81ead4e.vpn.example.com

I wish to create a reverse proxy on the Gatway to proxy requests, running nginx.

The plan is to create a DNS record for the gateway, *.gateway.com, and route (ahem, proxy) traffic going to/from 12a4f81ead4e.gateway.com to 12a4f81ead4e.vpn.example.com. And so the end-user would just need to type 12a4f81ead4e.gateway.com into their browser to access their device.
I'd like to use nginx, as the gateway is already running nginx for other purposes.

I expect HTTP requests to be easy, and can be done with a carefully crafted nginx proxy_pass directive.

But what about HTTPS requests? As far as I understand, TLS passthrough based on SNI is now implemented by nginx, but all the examples I've seen so far create a static map for … well mapping the incoming SNI to a target upstream:

stream {
  map $ssl_preread_server_name $selected_upstream {
    example.org upstream_1;
    example.net upstream_2;
    example.com upstream_3;
    default upstream_4;
  }
  upstream upstream_1 { server 10.0.0.1:443; }
  upstream upstream_2 { server 10.0.0.2:443; }
  upstream upstream_3 { server 10.0.0.3:443; }
  upstream upstream_4 { server 10.0.0.4:443; }
  server {
    listen 10.0.0.5:443;
    proxy_pass $selected_upstream;
    ssl_preread on;
  }
}

Problem is devices are added/removed dynamically from the VPN, and I don't want to rewrite the nginx config files all the time. If reading the map from a file is possible, that's a step in the right direction, although I think nginx would need to be reloaded every time that changes, which raises permissions issues, that could be circumvented with sudo rules of course, but not the best solution.

Also I only want to proxy requests coming in to *.gateway.com, and server other https requests normally to the existing vhosts. If at all possile, I would like to avoid terminating the SSL connection. Not really a hard requirement, but would like to implement it that way if techinically doable. Also just for the kicks.

I'm fine internally listening on an alternate port for the other vhosts, I did something similar for HTTP when I wanted to set a "global" location, and moved all HTTP vhosts to port 81, and implemented a catch-all vhost on port 80 that served the "global" location, and proxied everything else to port 81. 🙂

So… What I would need it something like this (obviously not working):

stream {
  map $ssl_preread_server_name $selected_upstream {
    (.*).gateway.com $1.vpn.example.com;
    default normal_serve;
  }

  upstream normal_serve { server 127.0.0.1:8443; }

  server {
    listen 0.0.0.0:443;
    proxy_pass $selected_upstream;
    ssl_preread on;
  }

  server {
    listen 127.0.0.1:8443;
    server_name other.website.com;

    (...)
  }
}

Best Answer

This does the trick:

stream {
  resolver 8.8.8.8;

  map $ssl_preread_server_name $selected_upstream {
    ~(.*).gateway.example.com $1.vpn.example.com:443;
    default 127.0.0.1:8443;
  }

  server {
    listen 0.0.0.0;
    proxy_pass $selected_upstream;
    ssl_preread on;
  }
}

http {
  resolver 8.8.8.8;
  server {
    listen 127.0.0.1:8443 ssl;
    (...)
  }
}