How to Configure Nginx to Reject SSL Requests for Unlisted Virtual Servers


2 views

When using a wildcard SSL certificate (*.domain.com) on a single IP address hosting multiple subdomains, Nginx's default behavior presents a security concern. By default, if no server_name matches the request, Nginx will serve the first defined virtual host for that port, potentially leaking sensitive information.

The fundamental issue stems from how SSL/TLS works - the Host header (which contains the server_name) is encrypted during the SSL handshake. Nginx must complete the handshake before it can inspect the Host header, which means it must use the default certificate for the initial response.

Here's the proper configuration that achieves silent connection termination for invalid hosts:

# Primary SSL configuration
ssl_certificate     /etc/nginx/tls/wildcard.domain.crt;
ssl_certificate_key /etc/nginx/tls/wildcard.domain.key;

# Valid virtual host
server {
    listen 443 ssl;
    server_name app1.domain.com;
    
    location / {
        return 200 "This is app1.domain.com";
        add_header Content-Type text/plain;
    }
}

# Valid virtual host
server {
    listen 443 ssl;
    server_name app2.domain.com;
    
    location / {
        return 200 "This is app2.domain.com";
        add_header Content-Type text/plain;
    }
}

# Catch-all for unmatched hosts
server {
    listen 443 ssl default_server;
    server_name _;
    ssl_reject_handshake on;
}

The magic happens with these critical directives:

  • default_server marks this as the fallback virtual host
  • ssl_reject_handshake on (Nginx 1.19.4+) terminates SSL immediately
  • server_name _ is just a placeholder for invalid hosts

If you're running Nginx < 1.19.4, use this alternative approach:

server {
    listen 443 ssl default_server;
    server_name "";
    return 444;
}

server {
    listen 443 ssl;
    server_name invalid.*;
    return 444;
}

Test your configuration using OpenSSL:

openssl s_client -connect your.server:443 -servername invalid.domain.com

For valid hosts, you should see the SSL handshake complete. For invalid hosts, the connection should terminate immediately with no response.

While this solution works, remember that Nginx still needs to:

  1. Accept the TCP connection
  2. Perform the SSL handshake
  3. Parse the SNI (Server Name Indication)

Only then can it reject the connection. For high-security environments, consider separating services onto different IP addresses when possible.


When running multiple SSL-enabled virtual hosts on a single IP address with a wildcard certificate, you'll encounter a fundamental Nginx behavior: it will always serve the first matching SSL virtual host if the requested hostname isn't explicitly defined. This creates a security and privacy concern where "unlisted" domains appear to be served by your server.

Common approaches like return 444 or deny all in a catch-all server block don't work as expected because:

  1. Nginx must complete the SSL handshake before processing these directives
  2. The client still receives TLS-level responses, revealing server existence

Here's the proper configuration that completely drops connections for unknown hosts:

# Wildcard certificate for all subdomains
ssl_certificate     /etc/nginx/tls/wildcard.domain.crt;
ssl_certificate_key /etc/nginx/tls/wildcard.domain.key;

# Known valid host
server {
    listen 443 ssl default_server;
    server_name valid.domain.com;
    # Your normal configuration here
    return 200 "Valid host";
}

# Reject all other hosts at SSL level
server {
    listen 443 ssl;
    server_name _;
    ssl_reject_handshake on;
}
  • ssl_reject_handshake on: This directive (introduced in Nginx 1.19.4) is the magic that makes this work by rejecting connections during the SSL handshake
  • Order matters: The default server must be listed first
  • No response: The connection gets terminated without any byte being sent

If you're running Nginx < 1.19.4, use this alternative approach:

server {
    listen 443 ssl default_server;
    server_name _;
    ssl_certificate /path/to/dummy.crt;
    ssl_certificate_key /path/to/dummy.key;
    return 444;
}

Create a self-signed dummy certificate that doesn't match your domain to force connection rejection during handshake.

Verify the behavior with OpenSSL:

openssl s_client -connect your.server:443 -servername unknown.domain.com

For valid hosts you should see successful handshake, for invalid hosts - immediate connection drop.