Nginx with multiple domains/subdomains and ssl_certificate variable

We have a Nginx with hundred of proxies to several domains/subdomains. The way it being held during years, each new domain, we just go there and create a new conf file with server block containing HTTP and HTTPS.

Notice a lot of those server blocks have same proxy parameters, only changing the backend, and was like "hmm, could i try put all this in a single pair of server block with http and https?" and decide give it a try :)

My idea was to map variables with address to the backend and to the certificated used, since those are the only info that changes on mostly of the server blocks we have, this way for new domains would just be add the lines on the maps.

HTTP just work fine, no problems at all, HTTPS is the problem.

I have multiple domains, like "example02.com, example01.com, ..." and use wildcard certificate for each of those domains, so based on the host, I map the certificate name to a variable and use it on the ssl_certificate and ssl_certificate_key just like below, wich i saw its only possible since Nginx 1.15, looks like. (I am using 1.18 right now)

The problem is that, with the way i build the below server block, only the www.example01.com will work with the correctly certificate, when you try to access www.example02.com, it will still try to load the *.example01.com certificate.

Could just create a server block for each domain to server multiple subdomains with different certificates, but for now i just trying figure out if it possible to do it with just a pair of server blocks and checking if i doing something wrong.

map $host $backendaddr {
        default         "127.0.0.1:4444";
        www.example01.com   "127.0.0.1:4445"
        www.example02.com   "127.0.0.1:4446"
}

map $host $certificate {
        default         "example.com";
        www.example01.com   example01.com;
        www.example02.com   example02.com;
}

server {
   listen 80;
   server_name www.example01.com www.example02.com;
   access_log /var/log/nginx/access-http.log main;

   proxy_set_header Host $host;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_connect_timeout 1800;
   proxy_read_timeout 1800;
   proxy_send_timeout 1800;
   location / {
      proxy_pass http://$backendaddr$request_uri;
   }
}

server {
   listen 443 ssl;
   server_name  www.example01.com www.example02.com;
   access_log /var/log/nginx/access-https.log main;
   
   ssl_certificate ssl/$certificate.pem;
   ssl_certificate_key ssl/$certificate.key;

   proxy_set_header Host $host;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_connect_timeout 1800;
   proxy_read_timeout 1800;
   proxy_send_timeout 1800;
   location / {
      proxy_pass https://$backendaddr$request_uri;
   }
}

The problem is that $host variable is used in the map for certificates. The request flow in TLS connection is something like following:

  1. Client sends TLS Hello packet to server after TCP handshake. Hello packets includes SNI field, which tells the virtual host client tries to connect to.
  2. Server needs to select a certificate to send for the client. $host variable is only populated from the following:
  • Host name from the request line
  • Host name from the Host request header
  • server_name matching the request

The only available information here is server_name, and therefore www.example01.com is used as the value for $host variable.

To reach your objective, you can try following:

server {
    listen 443 ssl;
    server_name www.example01.com www.example02.com;
    ssl_preread on;
    ...
}

map $ssl_preread_server_name $certificate {
    default           example.com;
    www.example01.com example01.com;
    www.example02.com example02.com;
}

ssl_preread on makes nginx assign variables based on TLS ClientHello packet. $ssl_preread_server_name contains the value of SNI field from ClientHello packet, which can be then used to select the certificate.