Security Hardening
Rate limiting with limit_req, hiding server tokens, and common misconfigurations to avoid.
Rate limiting with limit_req
Without any rate limiting, a single client — a misbehaving script, a brute-force login attempt, a scraper — can send requests as fast as the network allows, consuming resources meant for everyone else. limit_req caps how many requests a client (identified by a key you choose, typically its IP address) can make in a given time window:
http {
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
listen 80;
server_name example.com;
location /login {
limit_req zone=login_limit burst=3 nodelay;
proxy_pass http://127.0.0.1:3000;
}
location /api/ {
limit_req zone=api_limit burst=20;
proxy_pass http://127.0.0.1:3000;
}
}
}
limit_req_zone— declared once in thehttpblock, defines a shared memory zone tracking request counts per key.$binary_remote_addr(the client's IP, stored compactly) is the usual key;zone=login_limit:10mnames the zone and allocates 10MB (enough to track roughly 160,000 distinct IPs);rate=5r/msets the sustained limit — 5 requests per minute here, deliberately strict for a login endpoint that has no legitimate reason to be hit rapidly.limit_req— applied inside alocation, activates the named zone for that route.burst=3allows up to 3 requests beyond the steady rate to queue briefly rather than being rejected outright, absorbing a short, legitimate burst (a user double-clicking submit) without penalty.nodelayserves burst requests immediately instead of artificially delaying them to smooth out the rate — appropriate for a login form where you'd rather reject quickly than hold connections open.
A request exceeding the configured limit gets an immediate 503 Service Unavailable by default (configurable via limit_req_status) — the client is refused before ever reaching the backend at all, which is exactly the point: absorbing abusive traffic at the proxy layer protects the application server behind it from ever having to handle the load.
for i in $(seq 1 10); do curl -s -o /dev/null -w "%{http_code}\n" https://example.com/login; done
# 200
# 200
# 200
# 503 <- burst exhausted, further requests rejected until the window resets
# 503
Different endpoints legitimately need different limits — notice the example above sets a much stricter rate on /login (a classic brute-force target) than on the general /api/ prefix, rather than one blanket limit applied everywhere.
Hiding server tokens
By default, Nginx includes its exact version number in every response's Server header and on its default error pages — information that's genuinely useful to an attacker scanning for servers running a version with a known, unpatched vulnerability, and of no benefit at all to a legitimate client:
http {
server_tokens off;
}
curl -I https://example.com
# Before: Server: nginx/1.24.0
# After: Server: nginx
server_tokens off; removes the version number specifically while still identifying the software as Nginx — it's a small, low-cost step (not a real security boundary on its own, since hiding a version number doesn't patch anything) that at least removes free reconnaissance information from every single response by default.
Common misconfigurations to avoid
- Leaving default/example server blocks enabled. A fresh Nginx install often ships a default site (serving a generic welcome page, or worse, directory listing) still enabled — removing or replacing it prevents it from responding to requests for hostnames you never intended to serve at all.
- Enabling
autoindexon a directory that shouldn't be browsable.autoindex on;turns alocationwith no matching index file into a raw directory listing — appropriate occasionally for an intentional file-download directory, a serious information leak almost everywhere else (it can expose backup files,.gitdirectories, or configuration left in a web root by mistake). - Not restricting access to sensitive paths. Configuration files,
.envfiles, or a.gitdirectory accidentally left inside a web root'srootpath are directly downloadable unless explicitly blocked:
location ~ /\.(git|env) {
deny all;
return 404;
}
- Missing security response headers. A few headers cost nothing to add and close off common browser-side attack classes:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
X-Frame-Options prevents the page from being embedded in a hostile <iframe> on another site (mitigating clickjacking); X-Content-Type-Options: nosniff stops a browser from guessing a response's content type against what the server actually declared (mitigating a class of MIME-confusion attacks); Referrer-Policy limits how much of the current URL leaks to external sites your page links to.
- Trusting
X-Forwarded-Forfrom any client directly, when Nginx is the first hop a request reaches. A malicious client can simply set its ownX-Forwarded-Forheader to anything it wants; it should only be trusted when it's known to have been appended by your own infrastructure (an upstream Nginx or load balancer you control), not accepted verbatim from the public internet — thereal_ipmodule'sset_real_ip_fromdirective scopes exactly which upstream addresses are trusted to supply it.
Common mistakes
- Applying one
limit_reqrate to every route uniformly instead of setting stricter limits on genuinely sensitive endpoints (login, password reset, checkout) and looser ones elsewhere. - Treating
server_tokens offas meaningful security rather than a small reconnaissance-reduction step — the underlying software still needs to actually be patched and kept up to date regardless. - Leaving
autoindexenabled, or forgetting to explicitly deny access to dotfiles and version control directories that might exist inside a web root by accident. - Trusting client-supplied
X-Forwarded-Forunconditionally on a server that's the first hop from the public internet — this lets any client trivially spoof whatever IP it wants for logging or IP-based rate limiting to see.