109

I have several servers running on the same machine, some with http only, some with both http and https. There are several server blocks defined in separate files which are included from the main config file.

I have set up a "default" server for http which will serve a generic "maintenance page" to requests that don't match any of the other server_names in the other config files. The http default server works as expected, it uses the server_name "_" and it appears first in the list of includes (because I have observed that in the case of duplicate server_names across servers, the one appearing first is used). This works great.

I would expect the same exact server block (only switching "listen 80 default_server" to "listen 443 default_server" and also instead of serving page "return 444") however it does not. Instead, it appears that the new default https server is actually grabbing all incoming https connections and causing them to fail, although the other server blocks have more appropriate server_names for the incoming requests. Removing the new default https server will cause semi-correct behavior to resume: the websites with https will all load correctly; but the websites without https will all be routed to the first https server in the include files (which according to the docs, if no "default_server" appears, then the first server block to appear will be "default").

So my question is, what is the correct way to define a "default server" in nginx for ssl connections? Why is it that when I explicitly set a "default_server" it gets greedy and grabs all connections whereas when I implicitly let nginx decide the "default server" it works like I would expect (with the incorrect server set as default and the other real servers behaving correctly)?

Here are my "default servers". Http works without breaking other servers. Https breaks other servers and consumes all.

server {
    listen 443 ssl default_server;
    server_name _;

    access_log /var/log/nginx/maintenance.access.log;
    error_log /var/log/nginx/maintenance.error.log error;

    return 444;
}

server {
    listen *:80 default_server;
    server_name _;
    charset utf-8;

    access_log /var/log/nginx/maintenance.access.log;
    error_log /var/log/nginx/maintenance.error.log error;

    root /home/path/to/templates;

    location / {
        return 503;
    }

    error_page 503 @maintenance;

    location @maintenance {
        rewrite ^(.*)$ /maintenance.html break;
    }
}

Any of you see what might be wrong here?

0

10 Answers 10

49

I managed to configure a shared dedicated hosting on a single IP with nginx. Default HTTP and HTTPS serving a 404 for unknown domains incoming.

1 - Create a default zone

As nginx is loading vhosts in ascii order, you should create a 00-default file/symbolic link into your /etc/nginx/sites-enabled.

2 - Fill the default zone

Fill your 00-default with default vhosts. Here is the zone i am using:

server {
    server_name _;
    listen       80  default_server;
    return       404;
}


server {
    listen 443 ssl;
    server_name _;
    ssl_certificate /etc/nginx/ssl/nginx.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx.key;
    return       404;
}

3 - Create self signed certif, test, and reload

You will need to create a self signed certificate into /etc/nginx/ssl/nginx.crt.

Create a default self signed certificate:

sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx.key -out /etc/nginx/ssl/nginx.crt

Just a reminder:

  • Test the nginx configuration before reloading/restarting : nginx -t
  • Reload a enjoy: sudo service nginx reload

Hope it helps.

7
  • 3
    Doesn't solve the browser warning of visiting an insecure site.
    – gdbj
    May 8, 2017 at 16:38
  • 7
    Can't be resolved since a catch-all must match any domains. No SSL can wildcard all domains. Imagine if you're spoofing google.fr address on your server, you will be able to authenticate your server as google.fr. It would be a serious security issue :(
    – Ifnot
    May 9, 2017 at 14:22
  • 2
    Ya, I guess that makes sense. Unfortunately in Chrome, you are presented a scary warning before seeing the 404 page which is worse than having the server simply reject the traffic. Makes it look like the server is misconfigured.
    – gdbj
    May 9, 2017 at 14:43
  • 1
    Thank you very much. This worked for me, except that I had to add default_server for listen 443 and I added the IPv6 address [::]:80 and [::]:443 with default_server.
    – chmike
    Apr 17, 2019 at 12:45
  • 4
    mkdir /etc/nginx/ssl was required first for the openssl command
    – wal
    Nov 14, 2019 at 3:07
31

You do not have any ssl_certificate or ssl_certificate_key defined in your "default" https block. Although you do not have or want a real key for this default scenario, you still need to configure one or else nginx will have the undesired behaviour that you describe.

Create a self-signed certificate with a Common Name of * and plug it into your config and it will start to work as you want.

The "default" behaviour under this set up would be that a browser would get a warning that the certificate can not be trusted, if the user adds the certificate as an exception, the connection will be dropped by nginx and they will see their browser's default "could not connect" error message.

1
  • 1
    I've tried this but it still doesn't work: All ssl requests to the IP go to my other ssl host. Anything else I could try? Aug 18, 2015 at 11:17
20

We basically want to avoid at all cost that the first server definition in our config file is served as a catch-all-server for SSL connections. We all know that it does that (opposed to http and using default_server config which works nicely).

This cannot be achieved declaratively for SSL (yet) so we have to code it with an IF...

The variable $host is the host name from the request line or the http header. The variable $server_name is the name of the server block we are in right now.

So if these two are not equal then you served this SSL server block for another host so that should be blocked.

The code does not contain specific references to your server IP addresses so it can easily be reused for other server configs without modification.

Example:

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

    ###
    ### Section: SSL

    #
    ## Check if this certificate is really served for this server_name
    ##   http://serverfault.com/questions/578648/properly-setting-up-a-default-nginx-server-for-https
    if ($host != $server_name) {
        #return 404 "this is an invalid request";
        return       444;
    }

    ...
2
  • could you explain what the listen [::]:443 ssl http2; does? i'm having trouble finding documentation for it. Aug 9, 2016 at 15:31
  • I think I found it, just needed to know what to search for. IPV4 and IPV6 directives. Aug 9, 2016 at 15:39
15

To elaborate more on the Radmilla Mustafa's answer:

Nginx uses 'Host' header for server_name matching. It does not use TLS SNI. This means that for an SSL server, nginx must be able to accept SSL connection, which boils down to having certificate/key. The cert/key can be any, e.g. self-signed.

See documentation

Hence, the solution is:

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 <path to cert>;
    ssl_certificate_key <path to key>;
    return 404; # or whatever
}
2
  • I feel that this should be the accepted answer. Many thanks. Apr 15, 2018 at 23:46
  • Simple, clean configuration and works perfectly. Thanks.
    – mahfuz
    Nov 15, 2019 at 9:39
8

If you just want to make sure no data is returned, you can use the following snippet without certificates:

map "" $empty {
        default "";
}

server {
        listen 80 default_server;
        listen 443 ssl http2 default_server;
        listen [::]:80 default_server;
        listen [::]:443 ssl http2 default_server;

        server_name _;

        ssl_ciphers aNULL;
        ssl_certificate data:$empty;
        ssl_certificate_key data:$empty;

        return 444;
}
3
  • Thank you, this worked perfectly for me. Jul 20, 2021 at 19:25
  • Unfortunately, this gives me BIO_new_file("/etc/nginx/data:$empty") failed (SSL: error:02001002:system library:fopen:No such file or directory:fopen('/etc/nginx/data:$empty','r') error:2006D080:BIO routines:BIO_new_file:no such file). Sep 9, 2021 at 15:02
  • 2
    Apparently the map … thingy can be replaced with a simple set $empty "" just before the ssl_certificate directive. It works for me with Nginx 1.18.0. Nov 20, 2021 at 12:23
5

To anyone who lost as much hair over this as me (spent nearly whole day on this today). I have tried almost everything, and the thing that made it finally working correctly was this stupid line:

ssl_session_tickets off;

Building upon Ifnot's answer, my working example is:

server {
    listen 443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server;
    server_name _;

    ssl_certificate /etc/nginx/ssl/nginx.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx.key;
    ssl_session_tickets off;

    return 404;
}

I have no idea why this was necessary, the only principle I derived out of this was that nginx behaves very strangely when we don't give him what he wants.

2
  • 1
    you are a legend. Feb 11, 2019 at 17:03
  • the default is 5min, I guess a little bit of patience would have done the job too :-)
    – Rmatt
    Dec 18, 2020 at 15:45
2

If you want to be absolutely sure, then use separate IP addresses for hosts which should not answer on HTTPS and hosts which should. This also resolves the "invalid certificate" browser warning problem.

2

The ssl_reject_handshake on directive in Nginx is used to reject SSL/TLS connections. This directive was introduced in Nginx version 1.19.4 - as far as I know.

When ssl_reject_handshake is set to "on", Nginx will immediately terminate the SSL/TLS handshake. As a result, no HTTP status code is sent, as the connection is terminated before the HTTP protocol is established.

Instead of an HTTP status code, the client will receive an SSL/TLS connection error and will see a corresponding error message depending on the browser and operating system used. The exact error message may vary, but it could be something like "SSL connection failed" or "SSL/TLS handshake failed".

Here's an example configuration that uses ssl_reject_handshake to reject connections from clients:

server
{
    listen 443 ssl http2 default_server;
    listen [::]:443 ssl http2 default_server;
    server_name _;

    ssl_certificate /etc/nginx/ssl/nginx.crt;
    ssl_certificate_key /etc/nginx/ssl/nginx.key;
    ssl_session_tickets off;
    ssl_reject_handshake on;
}

It's important to note that enabling ssl_reject_handshake can impact the usability of your website or application, as some clients may not be able to establish a connection. Therefore, it's important to test thoroughly and ensure that the criteria you set are appropriate for your specific use case.

One advantage is that obsolete domains do not display a certificate error in the browser, but instead appear to be truly offline.

Seems to work. Will not generate access log entry.

2

I found Ivan Shatsky's answer on 6/2022 on Stackoverflow the most informative, and it almost applies verbatim here too:

You need only the single default server block to catch everything else that is undefined in other server blocks. You don't need to expose any of your real certificates in that block; use the dummy self-signed certificate/key instead for the security purposes. You don't need to use any server_name at all in that block; moreover, that _ doesn't act as a wildcard at all. You can see that

server_name _;

in some nginx configurations from time to time due to the historical reasons because before nginx 0.7.12 you was required to specify something as server_name, which isn't required nowadays anymore. More information provided by the official documentation:

In catch-all server examples the strange name "_" can be seen:

server {
    listen       80  default_server;
    server_name  _;
    return       444;
}

There is nothing special about this name, it is just one of a myriad of invalid domain names which never intersect with any real name. Other invalid names like "--" and "!@#" may equally be used.

An example of a catch-all block can be the following one:

server {
    listen 80 default_server;
    listen [::]:80 default_server;
    listen 443 default_server ssl;
    listen [::]:443 default_server ssl;
    ssl_certificate /some/path/any.crt;
    ssl_certificate_key /some/path/any.key;
    return 444; # silently drop the connection
    # or you can define some landing page here
}

For generating a pair of self-signed key/cert in one line you can use the following command:

openssl req -nodes -new -x509 -subj "/CN=localhost" -keyout /some/path/any.key -out /some/path/any.crt

A good practice is to put this stub server block as the /etc/nginx/conf.d/default.conf or /etc/nginx/sites-enabled/default file contents, depending on the vhosts serving style you are actually using (a long read discussion on this subject can be found here at the ServerFault).

If you are using certbot, don't allow it to auto-generate redirect server blocks for you in case of using such a stub server block. Since everything with a non valid request hostname will be handled by the stub block, instead of something like

server {
    if ($host = example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot
    if ($host = www.example.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot

    listen 80;
    listen [::]:80;

    server_name example.com www.example.com;
    return 404; # managed by Certbot
}

use the following one:

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
} 

To redirect non-www to www and HTTP to HTTPS, use three server blocks for each hosted vhost:

server {
    listen 80;
    listen [::]:80;
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.com;
    # ssl certificate/key for 'example.com' domain here
    return 301 https://www.example.com$request_uri; 
}

server {
    listen 80;
    listen [::]:80;
    server_name www.example.com;
    return 301 https://www.example.com$request_uri; 
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.example.com;
    # ssl certificate/key for 'www.example.com' domain here
    # the main configuration part
    # ... 
}
1

To support multiple HTTPS server_name, the Nginx version must support TLS SNI.

TLS SNI

SNI allows browser to pass requested server name during the SSL handshake

Check if Nginx support TLS SNI

$ nginx -V
...
TLS SNI support enabled
...

and check the error_log that without this warning

nginx was built with SNI support, however, now it is linked
dynamically to an OpenSSL library which has no tlsext support,
therefore SNI is not available

Official HTTPS document has more detail.

If Nginx disable TLS SNI

Nginx will use default server certificate for all request

Official HTTPS document

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.

Testing config

server {
    # default server
    # if Nginx does not support SNI, Nginx will use this for all request
    listen       443 ssl default_server;
    server_name  "";

    ssl_certificate      default.crt;
    ssl_certificate_key  default.key;

    return 200 "default page";
}

server {
    listen       443 ssl;
    server_name  a.example.org;

    ssl_certificate      a.crt;
    ssl_certificate_key  a.key;

    return 200 "a.example.org page";
}

server {
    listen       443 ssl;
    server_name  b.example.org;

    ssl_certificate      b.crt;
    ssl_certificate_key  b.key;

    return 200 "b.example.org page";
}
# Add -v to verify the certificate

$ curl --insecure --resolve "a.example.org:443:127.0.0.1" https://a.example.org

a.example.org page

$ curl --insecure --resolve "b.example.org:443:127.0.0.1" https://b.example.org

b.example.org page

$ curl --insecure https://127.0.0.1

default page

Ref: Nginx TLS SNI

You must log in to answer this question.