Nginx and https - Specifying an ip address as a server_name gives the correct website but the wrong certificate

I want to run this url: https://192.168.1.254 and get a website with the correct content and certificate in the address bar. I am getting the website but I get an invalid certificate error in the address bar because the cert is taken from a different server block: the default server block 000-default.conf.

Can someone explain this behavior to me please?

My client browser is Google Chrome Version 87.0.4280.88 (Official Build) (64-bit)

My Nginx server is:

root@OpenWrt:/etc/nginx/conf.d# nginx -V
nginx version: nginx/1.19.4 (x86_64-pc-linux-gnu)
built with OpenSSL 1.1.1h  22 Sep 2020
TLS SNI support enabled

I think the issue is related to how SNI apparently does not allow a Literal IPv4 and IPv6 addresses as a "HostName". But is that really the case?

I have a default server block 000-default.conf like this:

server {
    server_name _;
    listen 80 default_server;
    listen 443 ssl default_server;

    ## To also support IPv6, uncomment this block
    # listen [::]:80 default_server;
    # listen [::]:443 ssl default_server;

    ssl_certificate '/etc/nginx/conf.d/_lan.crt';
    ssl_certificate_key '/etc/nginx/conf.d/_lan.key';
    return 404; # or whatever
}

And another server called luci-http.conf like this:

server {
        listen 80;
        listen [::]:80;
        server_name openwrt.lan 192.168.1.254;
        # access_log /proc/self/fd/1 openwrt; # use logd (init forwards stdout).
        include conf.d/*.locations;
}

When I put http://192.168.1.254 in the address bar it serves me up the correct webpage.

I also have this https server: luci-https.conf

server {
        listen 443 ssl;
        listen [::]:443 ssl;

        server_name openwrt.lan 192.168.1.254;
        #include '/var/lib/nginx/lan_ssl.listen.default';
        ssl_certificate '/etc/nginx/conf.d/_lan.crt';
        ssl_certificate_key '/etc/nginx/conf.d/_lan.key';
        ssl_session_cache 'shared:SSL:32k';
        ssl_session_timeout '64m';
        # access_log /proc/self/fd/1 openwrt; # use logd (init forwards stdout).
        include conf.d/*.locations;
}

When I put https://192.168.1.254 in the address bar it serves me up the correct webpage and the certificate in _lan.crt. As you can see I have the same Cert/Key pair in this and the default server block.

However when I remove that ip address as a server_name from luci-https.conf and add it as a server_name in: mysite.lan.conf I don't see the same behavior.

server {
        listen 443 ssl;
        listen [::]:443 ssl;
        #listen 192.168.1.254 ssl;
        #include '/var/lib/nginx/lan_ssl.listen';

        server_name mysite.lan www.mysite.lan fun.mysite.lan 192.168.1.254;

        root /www/mysite;
        index index.html index.htm index.nginx-debian.html;

        ssl_certificate '/etc/nginx/conf.d/mysite.lan.crt';
        ssl_certificate_key '/etc/nginx/conf.d/mysite.lan.key';
        ssl_session_cache 'shared:SSL:32k';
        ssl_session_timeout '64m';

        location / {
                try_files $uri $uri/ =404;
        }

        access_log /var/log/nginx/mysite.lan.access.log;
        error_log /var/log/nginx/mysite.lan.error.log;
}

Now when I put https://192.168.1.254 in the address bar it serves me up the correct webpage but again the certificate in _lan.crt not the certificate: mysite.lan.crt from mysite.lan.conf as expected.

When I put..

ssl_certificate '/etc/nginx/conf.d/mysite.lan.crt';
ssl_certificate_key '/etc/nginx/conf.d/mysite.lan.key';

in the default server block 000-default.conf then I get that certificate instead when I put https://192.168.1.254 in the browser address bar whether 192.168.1.254 is specified as a server_name in luci-https.conf or mysite.lan.conf.

So it seems that SNI will match on a "hostname" that is an ip address but it takes the certificate from the default server block. Why is that?


... SNI apparently does not allow a Literal IPv4 and IPv6 addresses as a "HostName". But is that really the case?

The idea behind SNI is to distinguish between multiple domains on the same IP address. Insofar using SNI with an IP address does not really make sense. Therefore it is also restricted to actual hostnames. To cite from RFC 6066:

"HostName" contains the fully qualified DNS hostname of the server, as understood by the client. ... Literal IPv4 and IPv6 addresses are not permitted in "HostName"

    server_name mysite.lan www.mysite.lan fun.mysite.lan 192.168.1.254;

...
So it seems that SNI will match on a "hostname" that is an ip address but it takes the certificate from the default server block.

Since SNI is only used for actual hostnames there will be no SNI inside the TLS handshake and thus the default HTTPS configuration gets used. Inside the HTTPS there is the HTTP protocol though which includes the Host header. Since the Host header specifies the IP address (because the URL does) it will match this specific virtual host. Therefore: wrong certificate, right content.