Nginx powers over 34% of the world's websites — it is the default web server on DigitalOcean Droplets and most cloud VPS. But its default configuration is not secure. No security headers, version exposed, no rate limiting. This guide shows you exactly what to change.
Out of the box, nginx is fast and reliable, but it is not configured for security. The default install exposes the server version in HTTP headers, sends no security headers, has no rate limiting, and serves directory listings if an index file is missing. Attackers use automated scanners to detect these defaults across millions of servers.
A misconfigured nginx server tells attackers exactly what version you run, which CVEs apply, and that you probably have not hardened anything else either. For WAF-level protection without compiling modules, see our comparison of Defensia vs ModSecurity and Cloudflare WAF. Every section below addresses a specific risk with a concrete configuration change.
By default, nginx sends a Server: nginx/1.24.0 header with every response. This tells attackers your exact version, which they can match against published CVE databases. Hiding the version is the simplest hardening step.
server_tokens off;
After this change, the Server header will show nginx without a version number. Error pages will also stop showing the version. Reload nginx with nginx -t && systemctl reload nginx.
Security headers instruct the browser to enforce protections against clickjacking, MIME-type sniffing, cross-site scripting, and protocol downgrade attacks. Nginx sends none of these by default. Add them to your server block or http block.
# Prevent clickjacking — only allow framing from same origin
add_header X-Frame-Options "SAMEORIGIN" always;
# Prevent MIME-type sniffing
add_header X-Content-Type-Options "nosniff" always;
# Enable XSS protection in older browsers
add_header X-XSS-Protection "1; mode=block" always;
# Force HTTPS for 1 year including subdomains
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# Restrict resource loading to same origin
add_header Content-Security-Policy "default-src 'self'" always;
# Disable browser features you don't use
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
# Control referrer information sent to other sites
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Prevents your page from being loaded in an iframe on another domain. Blocks clickjacking attacks where an attacker overlays an invisible frame to steal clicks.
Stops browsers from MIME-type sniffing. Without this, a browser might execute a malicious file disguised as a harmless MIME type.
Forces browsers to use HTTPS for all future requests. The max-age=31536000 directive keeps this active for one year. includeSubDomains extends it to all subdomains.
Controls which resources (scripts, styles, images) the browser is allowed to load. default-src 'self' only allows resources from your own domain. Customize for your needs.
Disables browser APIs you do not use. Prevents malicious scripts from accessing the camera, microphone, or geolocation even if injected via XSS.
Controls how much URL information is sent to other sites. strict-origin-when-cross-origin sends only the origin (not the full path) when navigating to another domain.
Without rate limiting, a single IP can send thousands of requests per second to your server, enabling brute force attacks on login pages, credential stuffing, and denial-of-service. Nginx has a built-in rate limiting module (ngx_http_limit_req_module) that requires just two directives.
# Define a rate limit zone (10MB shared memory, 10 requests/second per IP)
limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
# Apply to a location — allow bursts of 20, don't delay
location /login {
limit_req zone=one burst=20 nodelay;
}
How it works: $binary_remote_addr uses 4 bytes per IP (vs. 15+ bytes for the string), so 10MB stores approximately 160,000 IP states. The burst=20 parameter allows 20 requests above the rate before returning 503. The nodelay flag processes burst requests immediately instead of queuing them. Apply rate limiting to login pages, API endpoints, and any form submission handlers.
A properly configured TLS setup prevents protocol downgrade attacks, cipher suite weaknesses, and man-in-the-middle interception. Here is a production-ready configuration for 2026.
# Only allow TLS 1.2 and 1.3 — disable SSLv3, TLS 1.0, TLS 1.1
ssl_protocols TLSv1.2 TLSv1.3;
# Use server's cipher preference, not client's
ssl_prefer_server_ciphers on;
# Strong cipher suite (ECDHE for forward secrecy)
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384';
# Enable OCSP stapling for faster certificate validation
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
# Disable session tickets (prevents forward secrecy bypass)
ssl_session_tickets off;
# Session cache for performance
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
Why disable TLS 1.0 and 1.1? Both have known vulnerabilities (BEAST, POODLE, CRIME). All modern browsers support TLS 1.2+. PCI DSS 3.2+ requires TLS 1.2 minimum. Why disable session tickets? Session tickets reuse the same key for multiple connections, which breaks forward secrecy if the key is compromised. With ssl_session_cache, you get resumption performance without the security trade-off.
Every public web server receives thousands of requests for paths that do not exist: /.env, /.git/config, /wp-login.php (on non-WordPress servers), and /phpmyadmin. These are automated scanners looking for exposed credentials and known vulnerabilities. Block them explicitly to reduce noise and attack surface.
# Block access to hidden files and directories
location ~ /\. {
deny all;
return 404;
}
# Block WordPress probing on non-WP servers
location ~* ^/(wp-login|wp-admin|xmlrpc)\.php {
return 444;
}
# Block common scanner targets
location ~* ^/(phpmyadmin|myadmin|pma|dbadmin) {
return 444;
}
# Block by user-agent (known bad bots)
if ($http_user_agent ~* (Scanbot|Nikto|sqlmap|w3af|Nessus)) {
return 444;
}
Why return 444? This is an nginx-specific response that closes the connection without sending any data. It is faster than 403 or 404, gives the scanner no information, and does not generate an error page. Use 404 for hidden files so legitimate tools (like Let's Encrypt's .well-known) can be excluded with a preceding location block.
A Web Application Firewall (WAF) inspects HTTP requests for attack patterns like SQL injection, cross-site scripting, and path traversal. Nginx does not include a WAF by default. There are two main approaches.
| Feature | ModSecurity + CRS | Defensia WAF |
|---|---|---|
| Installation | Compile as nginx module | curl | bash (60 seconds) |
| Configuration | OWASP CRS rules (~200 files) | Zero config |
| Nginx changes required | Yes (load_module, modsecurity on) | None |
| Performance impact | Inline inspection (adds latency) | Reads logs async (zero overhead) |
| Attack types detected | 15+ (OWASP CRS) | 15+ (same OWASP types) |
| False positive tuning | Manual rule exclusions | Scoring engine (auto-threshold) |
| Blocking method | Inline (rejects HTTP request) | iptables (blocks all traffic from IP) |
| Dashboard | None (log files only) | Real-time web dashboard |
| Price | Free (open source) | Free (1 server), Pro EUR 9/mo |
ModSecurity is a powerful inline WAF, but it requires compiling as an nginx dynamic module, maintaining OWASP Core Rule Set (CRS) updates, and tuning rules to avoid false positives. It adds latency to every request because it inspects traffic inline. Defensia reads your nginx access.log asynchronously, detects the same attack patterns, and blocks offending IPs via iptables. Zero nginx configuration changes, zero performance overhead, and the same result: attackers get blocked. Learn more about Defensia WAF →
Your nginx access log is a goldmine of security intelligence. Every attack leaves a trace. Knowing what to look for turns your logs from noise into actionable threat data.
# SQL injection attempt
91.108.4.30 "GET /search?q=1'+OR+1=1-- HTTP/1.1" 200
# Path traversal to read /etc/passwd
103.145.13.90 "GET /../../../../etc/passwd HTTP/1.1" 400
# WordPress xmlrpc brute force
45.83.64.11 "POST /xmlrpc.php HTTP/1.1" 200
# Shell command injection
185.220.101.7 "GET /cgi-bin/;cat+/etc/shadow HTTP/1.1" 404
# Scanner probing for .env files
5.188.210.12 "GET /.env HTTP/1.1" 404
Manually reviewing logs is impractical at scale. Defensia reads nginx access logs automatically, applies 15+ OWASP detection patterns in real time, scores each IP based on attack severity and frequency, and blocks attackers via iptables within seconds. No log rotation scripts, no grep pipelines, no custom parsers. Just install the agent and your logs become an active defense layer.
Run a security header scan at securityheaders.com or use curl -I to inspect response headers. Check that Server header does not show a version number, security headers are present (X-Frame-Options, HSTS, CSP), and TLS is configured with modern protocols. For deeper analysis, Defensia monitors your nginx logs and alerts on attack patterns automatically.
No. Nginx does not include a WAF by default. You can add ModSecurity as a dynamic module with the OWASP Core Rule Set, but it requires compiling and maintaining the module. Alternatively, Defensia reads nginx access logs and detects the same OWASP attack types without any nginx configuration changes.
At minimum: X-Frame-Options (SAMEORIGIN), X-Content-Type-Options (nosniff), Strict-Transport-Security (max-age=31536000), Content-Security-Policy (restrict resource loading), Permissions-Policy (disable unused browser APIs), and Referrer-Policy. These protect against clickjacking, MIME sniffing, protocol downgrade, and cross-site scripting.
Use user-agent matching to block known scanner tools (Nikto, sqlmap, w3af), deny access to hidden files (location ~ /\.), and return 444 for common probe paths (/wp-login.php, /phpmyadmin, /.env). For comprehensive bot management with 70+ fingerprints and per-policy control, Defensia handles it automatically from your access logs.
No. Defensia reads your existing nginx access.log file. It does not modify your nginx configuration, does not sit in the request path, and does not require any module installation. The Go agent runs as a separate systemd service and blocks attackers via iptables/ipset.
OWASP attack detection from server logs.
Complete server hardening guide.
15 detection patterns, ipset blocking.
Hardening guide for Apache HTTP Server.
Web Application Firewall explained.
Full comparison: fail2ban vs Defensia.
Defensia reads your nginx logs and blocks attackers automatically. One command. Under 30 seconds.
No credit card required. Free for one server.