Nginx: rejecting unknown or specific domains over SSL

Under: Blog

Posted in nginx
Posted 3 years ago by James

With the acceptance of SNI it's possible to have multiple SSL domains hosted off the same IP address. Nginx will match the incoming HTTP Host header against the server_name in each server block and serve up the response based on the configuration it finds. There are a few gotchas though which can trip you up along the way.

One of these is the handshake, which happens before server_name recognition and is handled either by the first matching server block Nginx encounters OR by the server marked "default_server". The latter is better as it provides more control.

You can see the handshake in action by enabling a separate error log for each server and switching the error_log level to debug. You'll first see the request go to the default_server, the logging ending with an 'SSL server name: "domain.tld'" line before the request passes on to the server block that matches the server_name / incoming HOST header (domain.tld in this case).

To handle SSL requests on unknown or specific domains without serving up the content of another domain or having valid SSL domains use the wrong server block you need to do four things:

  1. Ensure that your default server has the default_server flag in its listen arguments
  2. Ensure that the default_server is listening on port 443 (or whichever is your SSL port)
  3. Ensure that your default server returns the Nginx specific 444 response code
  4. Ensure that your default server has a self signed or valid SSL certificate. Without this, Nginx will return an error in your error_log 'no "ssl_certificate" is defined in server listening on SSL port while SSL handshaking'


This is best illustrated with an example.
Say we have a server with one domain listening on ports 80 and 443 and an A Record in DNS that points the hostname of the server to the same IP address as the domain. Any request for the server hostname in the browser would go to the domain's server block by default.

First the default server config

# 002.default.conf
server {
	listen *:80 default_server;
	listen [::]:80 default_server ipv6only=on;
	listen *:443 default_server;
	listen [::]:443 default_server ipv6only=on;
	server_name _;
	access_log  /var/log/nginx/default.access.log main;
	error_log  /var/log/nginx/default.error.log debug;

        # a self signed cert as a fallback and to handle the 'no "ssl_certificate" is defined' error log message
        include /etc/nginx/snippets/snakeoil.conf;

	return 444;

Then a server handling requests to the hostname itself:

# conf.d/002.hostname.conf
server {
	listen *:80;
	listen [::]:80;
	listen *:443;
	listen [::]:443;
	server_name hostname.domain.tld;
	access_log  /var/log/nginx/hostname.access.log main;
	error_log  /var/log/nginx/hostname.error.log debug;

	return 444;

Now the actual domain being hosted

# sites-enabled/001.domain.tld.vhost
server {
        listen *:80;
	listen [::]:80;
	listen *:443 ssl http2;
	listen [::]:443 ssl http2;

	server_name domain.tld www.domain.tld;

	ssl_certificate /path/to/domain.tld.cert.pem;
	ssl_certificate_key /path/to/domain.tld.privkey.pem;

        # other specific SSL config here

	access_log /var/log/nginx/domain.tld.access.log main;
	error_log /var/log/nginx/domains.tld.error.log debug;

	# more config options for handling request, upstreams, optimisations, compression etc

In the main nginx.conf, along with all your other  options, make sure the conf.d files are loaded before any vhosts:

http {

        # general configuration entries up here

	include /etc/nginx/conf.d/*.conf;

	# any other other config including SSL ciphers and protocols accepted

	include /etc/nginx/sites-enabled/*.vhost;

What will happen here depends on the request (and you can see this in action with debug logging on) -

  • For SSL requests to the domain, the handshake will take place in the default config file then it will be passed over to the relevant server block matching the server_name where the correct and valid SSL certificate for the domain is sent to the browser. Non SSL requests will go straight to the domain's server block.
  • To avoid SSL requests on port 443 for the hostname going to the domain's SSL configuration and getting a certificate mismatch, the hostname listens on port 443 as well. The handshake happens in the default config then is terminated (the 444 code)
  • For requests from unknown domains (maybe a domain was deleted from the server and DNS hasn't been updated) these are handled in the default config for SSL or non-SSL requests regardless. SSL requests like this will get the snakeoil certificate and a browser warning page, which is better than them getting the content  from another domain.
  • For requests for a valid domain the server is hosting but does not serve over SSL, the non-SSL request will go to the relevant server block but the SSL request for the domain will end up in the default config as is required.

If and when you add a second domain with an SSL server block, this will rely on Nginx, OpenSSL and the browser having SNI support. All modern browsers and versions of Nginx and OpenSSL have this. If you are unwilling to drop old browser support e.g IE7 on WinXP then you'll have to serve the other domains off different IP addresses.

With that in place you should now have a catch-all server to make sure all those unknown requests get terminated without serving content from another domain's server config.


Post your comment


No one has commented on this page yet.

RSS feed for comments on this page | RSS feed for all comments