nginx - How to prevent processing requests with undefined server names with HTTPS
I've solved it by generating a fake certificate that doesn't reveal domain name and adding it as a default one on the start of the config:
server {
listen 443 default;
server_name _;
ssl on;
ssl_certificate /path/to/fake.crt;
ssl_certificate_key /path/to/fake.key;
return 403;
}
And yes, it requires a nginx with SNI support (check nginx -V
).
Follow on: How to create self signed SSL certificate for test purposes
Due to the nature of how SSL works, the SSL/TLS handshake is performed before the intended hostname is given to the web server. This means that the default (first) certificate is used when trying to access the site, regardless of the domain name used.
This is true with both Apache and nginx.
From the Apache Wiki:
As a rule, it is impossible to host more than one SSL virtual host on the same IP address and port. This is because Apache needs to know the name of the host in order to choose the correct certificate to setup the encryption layer. But the name of the host being requested is contained only in the HTTP request headers, which are part of the encrypted content. It is therefore not available until after the encryption is already negotiated. This means that the correct certificate cannot be selected, and clients will receive certificate mismatch warnings and be vulnerable to man-in-the-middle attacks.
From the nginx documentation:
With this configuration a browser receives the default server’s certificate, i.e. www.example.com regardless of the requested server name. This is caused by SSL protocol behaviour. The SSL connection is established before the browser sends an HTTP request and nginx does not know the name of the requested server. Therefore, it may only offer the default server’s certificate.
How can you resolve this issue?
The easiest solution is to use separate IP addresses for each site you wish to secure.
If this is not possible, it might be possible to resolve the issue using Server Name Indication (SNI, RFC 6066). This allows a browser to pass the domain name to the server during the handshake.
Both Nginx and Apache support SNI. You can find out more on nginx SNI in the documentation.
It's worth noting that SNI can only be used for domain names, and not IP addresses. You should take extra precaution when configuring your web servers to address this issue, so any request to the IP is handled properly.
Only domain names can be passed in SNI, however some browsers may erroneously pass an IP address of the server as its name if a request includes literal IP address. One should not rely on this.
The Apache Wiki has some more information on implemeting SNI. But even their documentation advises that this solution is not perfect.
Using name-based virtual hosts with SSL adds another layer of complication. Without the SNI extension, it's not generally possible (though a subset of virtual host might work). With SNI, it's necessary to consider the configuration carefully to ensure security is maintained.
With that said, you can see how this configuration isn't as simple as regular virtual hosts. In order to further come up with a solution to your problem, we would need to know more details on your configuration and the expected behavior when an IP only request is sent.
Generally, to 'block' a non configured domain or IP request, you would configure it as the default site and then display an error, redirect, exit, etc.
If I understand you want to deny HTTP-Requests, which don't contain a Host header, even if these requests are inside a SSL connection (e.g. https-Requests). These are old-style HTTP/1.0 requests, HTTP/1.1 requires a Host header but also most HTTP/1.0 clients already send one. Blocking these clients can be done with:
if ( $http_host = '' ) {
return 444;
}
But this does not help if the client uses a Host header with junk or the IP-address in it. Thus it would better to verify, that the host header contains the expected values (as a bonus this also helps against DNS rebinding attacks), e.g.
if ( $http_host !~* ^(example\.com|www\.example\.com)$ ) {
return 444;
}