Initializing Enclave...

How to Fix Nginx Conflicting Server Name '_' Warning and Add HSTS Headers

Threat/Impact Level: HIGH | Exploitability/Downtime Risk: HIGH | Time to Fix: 10 mins

TL;DR

  • What broke: Two server blocks both claim server_name _ on 0.0.0.0:80 — Nginx silently ignores the second one, meaning one virtual host is dead in production. Separately, no Strict-Transport-Security header is emitted on HTTPS responses, leaving users vulnerable to SSL-stripping MitM attacks.
  • How to fix it: Consolidate catch-all blocks to a single default_server directive and inject the HSTS header with a minimum max-age=31536000 in the SSL server block.
  • Shortcut: Use our Client-Side Sandbox below to auto-refactor this — paste your config and get a corrected diff without sending your server names or certs anywhere.

The Incident (What does the error mean?)

Raw Nginx error log output:

nginx: [warn] conflicting server name '_' on 0.0.0.0:80, ignored

And a security header audit returns:

Missing Header: Strict-Transport-Security

The warning fires at reload/start time. Nginx processes server blocks in config-load order. When it finds two blocks with identical server_name _ bound to the same address:port, it registers the first and silently drops the second. If your intended default catch-all was the second block, all unmatched traffic is now routing to the wrong virtual host — no error in access logs, no 5xx, just silent misrouting. This is the worst kind of production bug.

The HSTS omission is a separate, co-existing vulnerability. Without Strict-Transport-Security, a browser that visits your site over HTTP first — or a user on a captive portal — can be intercepted before the TLS handshake completes.


The Attack Vector / Blast Radius

Conflicting server name — blast radius: Silent misrouting means requests intended for your catch-all error handler or a security-hardened default block are instead handled by whichever _ block Nginx registered first. If that first block has permissive CORS, no rate limiting, or serves directory listings, you've just exposed those misconfigurations to all unmatched hostnames — including scanner traffic probing for Host: header injection targets.

Missing HSTS — exploit path: A network-level attacker (public Wi-Fi, BGP hijack, DNS poisoning) intercepts the initial HTTP request before the 301 redirect fires. Sslstrip or equivalent tools downgrade the connection to plaintext. Without HSTS pre-loaded in the browser, there is zero client-side enforcement of TLS. Session cookies marked Secure are never sent — but auth tokens in URL params or non-Secure cookies are transmitted in cleartext. CVSS score for HSTS absence in a login flow: 7.4 (High).

Combined risk: A misconfigured default block handling all unmatched traffic, with no transport security enforcement, is a trivial target for credential harvesting via a MitM proxy.


How to Fix It (The Solution)

Basic Fix

Find and remove the duplicate server_name _ block. Ensure exactly one block uses default_server on each address:port pair. Add HSTS to the SSL block.

# /etc/nginx/sites-available/default

- server {
-     listen 80;
-     server_name _;
-     return 301 https://$host$request_uri;
- }

  server {
-     listen 80;
-     server_name _;
+     listen 80 default_server;
+     server_name _;
      return 301 https://$host$request_uri;
  }

  server {
-     listen 443 ssl;
+     listen 443 ssl default_server;
      server_name _;
      ssl_certificate     /etc/ssl/certs/nginx.crt;
      ssl_certificate_key /etc/ssl/private/nginx.key;

+     # HSTS: force TLS for 1 year, include subdomains, allow preload submission
+     add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

      location / {
          root /var/www/html;
      }
  }

Enterprise Best Practice

In multi-vhost environments, never define catch-all blocks inline in sites-available. Use a dedicated conf.d/00-default.conf (the 00- prefix guarantees load order) and enforce HSTS at the upstream load balancer level (ALB, Cloudflare, etc.) so it cannot be accidentally omitted at the Nginx layer.

# /etc/nginx/conf.d/00-default.conf  (loaded FIRST by glob order)

+ server {
+     listen 80 default_server;
+     listen [::]:80 default_server;
+     server_name _;
+     # Redirect all plaintext to HTTPS — no content served here
+     return 301 https://$host$request_uri;
+ }
+
+ server {
+     listen 443 ssl default_server;
+     listen [::]:443 ssl default_server;
+     http2 on;
+     server_name _;
+
+     ssl_certificate     /etc/ssl/certs/default.crt;
+     ssl_certificate_key /etc/ssl/private/default.key;
+     ssl_protocols       TLSv1.2 TLSv1.3;
+     ssl_ciphers         HIGH:!aNULL:!MD5;
+
+     # HSTS with preload — submit to hstspreload.org after validating
+     add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
+     add_header X-Content-Type-Options "nosniff" always;
+     add_header X-Frame-Options "DENY" always;
+
+     # Catch-all returns 444 (no response) — kills scanner traffic
+     return 444;
+ }

Why return 444? Nginx's non-standard 444 code closes the TCP connection immediately with no response. Any scanner or bot probing random Host: headers gets nothing — no fingerprint, no error page, no timing data.


💡 Tired of pasting proprietary configs into ChatGPT? Generic AI tools log your company's ARNs, DB strings, and private keys. StackEngine is a zero-backend, pure Client-Side WASM utility. Drop your failing config into the sandbox above. We redact your secrets locally in the browser and auto-generate the refactored code using your own API key.


Prevention in CI/CD

1. nginx -t as a pre-commit gate

This catches the conflicting server name warning at lint time, before any deploy:

# .github/workflows/nginx-lint.yml
- name: Validate Nginx config
  run: docker run --rm -v $(pwd)/nginx:/etc/nginx:ro nginx:alpine nginx -t

Nginx exits non-zero on [warn] if you add -e stderr and pipe through grep -E '\[warn\]|\[error\]' with a || exit 1.

2. Checkov — IaC security scanning

checkov -d ./nginx --framework nginx
# Checks: CKV_NGINX_1 (HSTS), CKV_NGINX_2 (SSL protocols), CKV_NGINX_5 (server_tokens off)

3. Mozilla Observatory / securityheaders.com in CD pipeline

# Post-deploy smoke test
curl -sI https://your-domain.com | grep -i strict-transport
# Fails the pipeline if header is absent

4. OPA/Conftest policy for Nginx

# policy/nginx_hsts.rego
package nginx

deny[msg] {
    input.servers[_].add_header[_].name != "Strict-Transport-Security"
    msg := "HSTS header missing from SSL server block"
}

Run with conftest test nginx.conf --policy policy/ in your PR pipeline. Hard-block merges that remove the HSTS directive.

Related Diagnostics

"Part of the Security Utility Matrix."

View all 140 Security Tools →