When a request arrives with a Host header that matches none of your server_name entries — or no meaningful host at all, like a request to your bare IP — Nginx still has to answer. It uses the default server for that IP and port. If you haven’t chosen one, it’s simply the first server block loaded, which might be your most important site.

Why it matters

  • Scanners hit your IP constantly; without a catch-all, they see (and index) one of your sites under your IP address.
  • Someone can point their own domain at your IP, and your site appears under their name — duplicate content and potential phishing confusion.
  • A typo in a server_name silently serves the wrong site instead of failing clearly.

How Nginx chooses

For each request, Nginx:

  1. Selects the server blocks that listen on the IP:port the connection arrived on.
  2. Matches the Host header against their server_names (exact names, then wildcards, then regexes).
  3. If nothing matches, uses the block marked default_server for that IP:port — or, if none is marked, the first one defined.

A catch-all for HTTP

# /etc/nginx/sites-available/000-catchall
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    return 444;
}
  • server_name _; is just an invalid name that never matches anything; the default_server flag is what makes this the catch-all.
  • return 444 is Nginx-specific: close the connection without sending a response. Bots get nothing. (Use return 404; if you prefer a normal response.)

Enable it and remove Ubuntu’s stock default site (which also claims default_server):

sudo ln -s /etc/nginx/sites-available/000-catchall /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

Only one block per IP:port may have default_server, or nginx -t fails with “duplicate default server”.

A catch-all for HTTPS

HTTPS is trickier: the TLS handshake happens before Nginx sees the Host header, so the default server must present some certificate. Options:

Reject the handshake (Nginx 1.19.4+):

server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name _;
    ssl_reject_handshake on;
}

Clients connecting without a matching SNI name get a TLS error immediately — no certificate needed, nothing revealed.

Or use a self-signed certificate and return 444 (for older Nginx versions):

sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
  -keyout /etc/nginx/ssl/catchall.key -out /etc/nginx/ssl/catchall.crt -subj "/CN=invalid"
server {
    listen 443 ssl default_server;
    server_name _;
    ssl_certificate     /etc/nginx/ssl/catchall.crt;
    ssl_certificate_key /etc/nginx/ssl/catchall.key;
    return 444;
}

Test it

curl -sI http://203.0.113.10/                                # empty reply (444)
curl -sI -H "Host: not-mine.example" http://203.0.113.10/    # empty reply
curl -sI https://203.0.113.10/ -k                            # TLS handshake rejected
curl -sI https://example.com/                                # your real site still works

Exceptions you might need

  • Let’s Encrypt HTTP-01 challenges arrive with your real hostname, so they match your site’s server block — the catch-all doesn’t interfere.
  • Health checks from a load balancer that use the IP: give them a specific location in a dedicated server block, or check a real hostname.
  • Direct IP access for an app (rare): make that app’s block the default instead.

Debugging which block answered

sudo nginx -T | grep -nE "listen|server_name"

Add a temporary header in each server block — add_header X-Served-By "site-a" always; — and check with curl -I.

Related: Nginx server blocks, Nginx security headers and port scanning defenses.

  • Nginx
  • Security
  • Configuration