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
serverblocks both claimserver_name _on0.0.0.0:80— Nginx silently ignores the second one, meaning one virtual host is dead in production. Separately, noStrict-Transport-Securityheader 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_serverdirective and inject the HSTS header with a minimummax-age=31536000in 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 randomHost: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.