Nginx Security Configuration: Complete Hardening Guide
Nginx security configuration is one of the highest-leverage investments you can make as a web developer or sysadmin. A default Nginx installation exposes your server version, accepts outdated TLS protocols, and sends no security headers whatsoever. This guide fixes all of that with copy-paste configuration snippets you can apply in under an hour.
Before you start, run a free ZeriFlow scan to get a baseline. After applying these changes, scan again to confirm every improvement registered.
Hiding Server Information With server_tokens off
The first thing any attacker does is fingerprint your server. By default, Nginx includes its version number in HTTP response headers (Server: nginx/1.24.0) and in error pages. This tells attackers exactly which CVEs to try.
Add this to your http {} block in /etc/nginx/nginx.conf:
http {
server_tokens off;
}This removes the version number from the Server header (the header itself remains as Server: nginx, which is unavoidable without a paid module, but the version is gone). It also removes the version from default error pages.
For complete header removal, consider the nginx_more_headers module or a commercial Nginx Plus subscription, but server_tokens off is the essential first step that every server should have.
Verify:
curl -I https://yourdomain.com | grep ServerYou should see Server: nginx with no version number.
Enforcing TLS 1.3 and Disabling Legacy Protocols
TLS 1.0 and 1.1 are deprecated and vulnerable to BEAST, POODLE, and CRIME attacks. TLS 1.2 is still acceptable but TLS 1.3 is significantly faster (1-RTT handshake) and more secure (forward secrecy by default).
server {
listen 443 ssl;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
}Key decisions explained:
- ssl_prefer_server_ciphers off: Modern clients select the best cipher they support. Letting the client choose is now the recommended approach.
- ssl_session_tickets off: Session tickets can leak session data if your ticket keys are compromised. Disabling them improves forward secrecy.
- ssl_stapling on: OCSP stapling caches the certificate revocation status, eliminating a round trip to the CA and improving performance.
Security Headers: The Essential Set
Security headers tell browsers how to behave when rendering your content. Without them, browsers default to permissive behavior that enables XSS, clickjacking, and MIME sniffing attacks.
Add these to your server {} block:
add_header X-Frame-Options 'DENY' always;
add_header X-Content-Type-Options 'nosniff' always;
add_header X-XSS-Protection '1; mode=block' always;
add_header Referrer-Policy 'strict-origin-when-cross-origin' always;
add_header Permissions-Policy 'geolocation=(), microphone=(), camera=()' always;
add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload' always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://trusted-cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self'; frame-ancestors 'none';" always;What each header does:
- X-Frame-Options: DENY: Prevents your site from being embedded in iframes on other domains (clickjacking protection).
- X-Content-Type-Options: nosniff: Prevents browsers from MIME-sniffing responses away from the declared content type.
- Strict-Transport-Security: Forces HTTPS for the specified duration, even if the user types HTTP.
- Content-Security-Policy: Whitelists the sources from which resources can be loaded. This is the most powerful and most complex header — tune the script-src directive carefully for your specific application.
Important: The CSP above uses 'unsafe-inline' for scripts as a starting point. For maximum security, replace this with nonces or hashes for inline scripts. The always keyword ensures headers are sent even on error responses.
Rate Limiting: Stopping Brute Force and DDoS
Nginx's limit_req module allows you to throttle requests per IP address, which is essential for protecting login endpoints, APIs, and form submissions.
http {
# Define a zone: 10MB memory, 10 requests per second per IP
limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;
}
server {
# Apply general rate limiting to all requests
limit_req zone=general burst=20 nodelay;
location /login {
limit_req zone=login burst=5 nodelay;
# ... proxy or fastcgi pass
}
location /api/ {
limit_req zone=api burst=50 nodelay;
# ... proxy or fastcgi pass
}
}Parameters explained:
- burst=20: Allows a burst of up to 20 requests before rate limiting kicks in.
- nodelay: Processes burst requests immediately rather than queuing them, reducing latency for legitimate users.
- 10m memory: Stores state for approximately 160,000 IP addresses per 10MB. Size accordingly for your traffic.
When a client exceeds the rate limit, Nginx returns a 429 (Too Many Requests) status. You can customize this with limit_req_status 429; and a custom error page.
Blocking Common Attack Patterns
Beyond rate limiting, you can use Nginx's map and if directives to block known malicious patterns:
# Block empty user agents (most legitimate clients send a UA)
map $http_user_agent $bad_agent {
default 0;
'' 1;
'~*masscan' 1;
'~*nikto' 1;
'~*sqlmap' 1;
'~*nmap' 1;
}
server {
if ($bad_agent) { return 403; }
# Block access to hidden files (except .well-known for Let's Encrypt)
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}
# Block access to backup and config files
location ~* \.(bak|conf|dist|fla|inc|ini|log|psd|sh|sql|swp)$ {
deny all;
}
}Verifying With ZeriFlow
After applying these configuration changes, reload Nginx with nginx -t && systemctl reload nginx and then run a ZeriFlow scan to verify:
- All security headers are present and correctly formatted
- TLS configuration passes modern security standards
- No server version disclosure in response headers
- HTTP to HTTPS redirect is working correctly
- HSTS header is present with a sufficient max-age
ZeriFlow's 80+ checks will catch edge cases like headers missing from error responses, incorrect CSP syntax, or certificate chain issues that are easy to miss in manual testing.
FAQ
### Q: Can I put security headers in nginx.conf instead of individual server blocks?
A: Yes — place add_header directives in the http {} block for site-wide application. However, note that Nginx's inheritance model means if a server {} or location {} block defines any add_header directive, it overrides all headers from the parent block. Use a separate include file to avoid this pitfall.
### Q: What TLS ciphers should I disable completely? A: Never allow RC4, DES, 3DES, or NULL ciphers. Avoid MD5 and SHA-1 in cipher names. The Mozilla SSL Configuration Generator (ssl-config.mozilla.org) produces a regularly updated cipher list — use the 'Intermediate' profile for the best balance of compatibility and security.
### Q: Does rate limiting affect crawlers like Googlebot?
A: Rate limiting applies to all IP addresses equally. Googlebot typically crawls at a moderate, respectful pace that stays well under sensible rate limits. If you have concerns, you can exclude Googlebot's verified IP ranges from rate limiting using a geo block.
### Q: Should I use HTTP/2 or HTTP/3?
A: Enable HTTP/2 by adding http2 to your listen directive (listen 443 ssl http2). HTTP/3 requires the QUIC module, which is not in standard Nginx builds yet but is available in Nginx Plus and some distributions. HTTP/2 alone provides significant performance benefits over HTTP/1.1.
### Q: How do I test my CSP without breaking my site?
A: Use Content-Security-Policy-Report-Only instead of Content-Security-Policy during testing. This sends the same header but only reports violations to a specified endpoint rather than blocking them. Once you have zero violations in your logs, switch to enforcement mode.
Conclusion
Nginx security configuration is a multi-layered discipline. No single directive makes your server secure — it is the combination of version disclosure prevention, strong TLS, security headers, rate limiting, and attack pattern blocking that creates genuine defense in depth.
Apply these changes systematically, test after each change, and document your configuration in version control so you can reproduce it when you provision new servers.
Scan your Nginx server with ZeriFlow to get a comprehensive security report in under 60 seconds. The free scan covers all 80+ checks and gives you a prioritized remediation list specific to your server's current configuration.