Block direct access to webserver IP via HTTPS

Similar to this and this question I want to block users from using the IP to access my server.

For HTTP (port 80) this works fine, but not for HTTPS. So users can still enter https://<myip> to access the webserver and nginx returns the default certificate. BTW I use HTTP2 in my "usual" server blocks (with my domain names), so I use:

listen 443 ssl http2;
listen [::]:443 ssl http2;

I tried this to block HTTPS ip access now:

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

   server_name _;
   return 444;
}

However unfortunately this blocks all HTTPS requests no matter whether the domain is used or not. I know it might be necessary to block non-SNI clients as these of course do not deliver the used domain name to the server, so I am fine with that. (I mean clients not supporting SNI are old anyway...)

I generally prefer to block this in nginx, but if you would have some ideas about blocking this at the firewall level (iptables) I would also happily appreciate these too. And if you want to argue why blocking with iptables is better you can also do this and convince me to also block HTTP [or all other] requests to the IP too. Generally dropping the connection is fine (as nginx status code 444 does).

However there is one requirement: I do not want to explicitly mention the IP address of the server in the config as it is a dynamic IP and the server uses a dynamic dns service.

So in short, here is what I want to achieve:

  • block access via IP
  • allow access via domain name
  • it is fine to block non-SNI clients
  • it is fine to use a firewall blocking for this
  • dropping the connection is fine
  • without mentioning the IP address of the server
  • no exposing of the domain name via HTTPS

Edit: Another failed try. I tried to follow this suggestion and use this config snippet, which seems logically as a good thing:

if ($host != "example.com") {
        return 444;
}

It also basically works, but when I access "https://" I see that nginx at first already sends the HTTPS certificate (containing the domain name) and only when I skip the connection warning, I see that it blocks access. This is logical as nginx can only read the Host header when the HTTPS connection is there, but at this point nginx already send the server certificate containing the domain name, so a user now has the domain name and can reconnect using it making the whole IP blocking useless. I am also a bit concerned about the performance aspect of this solution as it causes nginx to check the host header for every request, so I am still looking for a solution here.


Today, I encountered the same problem with this block:

server {
  listen 443 ssl default_server;
  server_name <SERVER-IP>;
  return 444;
}

The nginx-logs says:

no "ssl_certificate" is defined in server listening on SSL port while SSL handshaking

As you mentioned, this seems to disable the other server-blocks as well. It 's possible to fix this by using a (self-signed) certificate for the server-ip-address. I did this using openssl:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout privateKey.key -out certificate.crt -subj '/CN=<SERVER-IP>'

Then change the server-block to:

server {
    listen 443 ssl default_server;
    server_name <server-ip>;
    ssl_certificate         /path/to/certificate.crt;
    ssl_certificate_key     /path/to/privateKey.key;
    return 444;
}

When someone uses the SERVER-IP over https to access the server, nginx presents the (self-signed) certificate and NOT the domain-name-certificate you want to hide.

By the way, make your server-block-with-IP the default_server. This way, a client who has SNI disabled, will get the IP-certificate and NOT the domain-name-certificate. This can also be tested with openssl (the -servername option, which will enable SNI, is omitted):

openssl s_client -connect <SERVER-IP>:443

Try this - the default_server bit is the important part. If it doesn't work please update your question showing the configuration for other servers listening on 443/SSL.

server {
  listen 443 ssl default_server;
  listen [::]:443 ssl default_server; # not sure if you want/need it here as well. Try both ways.

 server_name _;
 return 444;
}