Invalid HTTPS domain with no cert does a 301 redirect to the 1st configured server

My issue:

Sending a request to an HTTPS domain that is not configured in nginx but points to my server, you get the usual certificate error, but if you ignore that, nginx will send a 301 redirect to one of the configured servers (probably the 1st, but I haven’t investigated the order).

How I encountered the problem:

Some domain points to my IP for no reason.

Solutions I’ve tried:

Catch all for HTTP traffic works fine, but I can’t do a default_server for 443 without a cert. I could technically fetch the cert for the specific domain domain that is pointing over, but is that really the solution if the domain is something I don’t own or care about?

Version of NGINX or NGINX adjacent software (e.g. NGINX Gateway Fabric):

I tried on 1.18 and 1.24 so maybe there’s a change/fix in a later version?

Deployment environment:

Ubuntu 22-LTS, 24-LTS, cloud VM

Minimal NGINX config to reproduce your issue:

# catch-all
server {
listen 80 default_server;
server_name _;
return 444;
}

# Site something.tld with an http => https redirect
server {
listen 80;
server_name something.tld;
return 301 https://something.tld$request_uri;
}
server {
listen 443 ssl http2;
server_name something.tld;
# and all the ssl_ settings
}

Let’s assume the server IP is 1.2.3.4 and you have an /etc/hosts with this:

1.2.3.4 foobar.tld

Then you run curl test:

❱ curl -I http://foobar.tld
curl: (52) Empty reply from server

❱ curl -I https://foobar.tld
curl: (60) SSL: no alternative certificate subject name matches target hostname 'foobar.tld'

❱ curl -kI https://foobar.tld
HTTP/2 301
server: nginx
date: Thu, 16 Jul 2026 06:28:51 GMT
content-type: text/html
content-length: 162
location: https://something.tld

This last one is the one I have an issue with. How can I make it do nothing?

NGINX access/error log:

x.x.x.x - - [16/Jul/2026:09:19:31 +0200] "HEAD / HTTP/1.1" 444 0 "-" "curl/8.21.0" "-"
x.x.x.x - - [16/Jul/2026:09:18:32 +0200] "HEAD / HTTP/2.0" 301 0 "-" "curl/8.21.0" "-"

Hi there,

the basic “fix” for https is essentially the same as for http – create a server{} with the matching “listen” ip:port and mark it default_server. If you want to give the client some indication of what is wrong, then you would need a ssl_certificate and ssl_certificate_key; and because you do not expect any “real” requests to get to this service, you can safely use something like a self-signed certificate for a hostname like “go-away”. But if you want the client to “just” see that things failed mostly-silently, then the example for ssl_reject_handshake ( Module ngx_http_ssl_module ) will probably do what you want.

Note that this would “break” any https clients that do not use SNI; but I suspect that it is reasonable to call them broken anyway.

The reason that this works is that every server{} has at least one effective “listen” directive which names an ip:port. (There are default values, if they are not all explicitly listed.) When a http request comes in to an ip:port, nginx chooses which server{} to use based on the “listen” directives, and (in the common case) the “Host:” header – if “Host” does not match any relevant server_name, then the default_server (explicit, or implicit) for that ip:port is used.
When a https request comes in to an ip:port, the same applies except that the Host: header is not available early enough, and so nginx chooses based on the Server Name Indication field of the client side of the TLS negotiation.

The fuller details for selecting the server{} for http are described at How nginx processes a request while the SNI-handling is described towards the end of Configuring HTTPS servers

The example for ssl_reject_handshake is:

server {
    listen               443 ssl default_server;
    ssl_reject_handshake on;
}

server {
    listen              443 ssl;
    server_name         example.com;
    ssl_certificate     example.com.crt;
    ssl_certificate_key example.com.key;
}

where any https connection attempt to a name other than example.com will get an error response similar to what curl shows as

curl: (35) OpenSSL/3.0.20: error:0A000458:SSL routines::tlsv1 unrecognized name

Cheers,

Hi, Francis

thanks for the exhaustive reply! Dunno why I didn’t think about using a self signed cert but the ssl_reject_handshake is definitely the correct way to go for my case.