I have multiple domains on a single host, and nginx manages all of them. Each domain has it's own SSL certificate (which I get from certbot, using the "webroot" plugin).
I have a server block at the end of each config file, as a "catch-all" (from here and here), to return 404 for invalid subdomains.
Default nginx config file default.conf
:
# ...other config...
include /path/to/domain1.conf;
include /path/to/domain2.conf;
# ...other config...
domain1.conf
:
# redirect http to https
server {
listen 80;
listen [::]:80;
server_name domain1.com www.domain1.com
return 301 https://$host$request_uri;
}
# redirect naked to www
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name domain1.com
include path/to/ssl_config.conf
return 301 https://www.$host$request_uri;
}
# serve subdomain www
server {
listen 443 ssl http2;
listen [::]443 ssl http2;
server_name www.domain1.com
include path/to/ssl_config.conf
location / { proxy_pass http://$app; }
}
# catch-all for invalid subdomains (e.g. foo.domain1.com)
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
server_name: _.domain1.com
include path/to/ssl_config.conf
return 404;
}
domain2.conf
:
# same as above, but uses "domain2.com" instead of "domain1.com"
But that causes an error:
[emerg] a duplicate default server for xxx.xxx.xxx.xxx:443
If I remove those default_server
directives, then it doesn't route properly: a request to foo.example1.com
redirects to www.foo.example1.com
, then to www.www.foo.example1.com
, etc.
Everything works, except for the invalid subdomain logic. How can I fix it?
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.
Update @ 2024.11.06
Since Nginx 1.19.4, released on Oct 27, 2020, we can greatly simplify the catch-all block by using the ssl_reject_handshake
directive. This way, we don't need any keys or certificates, and an HTTPS request with the wrong server name passed through the TLS SNI extension will be terminated at a very early phase of the TLS handshake:
server {
listen 80 default_server;
listen [::]:80 default_server;
listen 443 default_server ssl;
listen [::]:443 default_server ssl;
ssl_reject_handshake on;
return 444;
}
For nginx versions before 1.19.4, an example of a catch-all block can be as follows:
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
...
}
(If you want to do the opposite, redirect www to non-www, just replace example.com
with www.example.com
and vise versa.)