Nginx Configuration and Security¶
Location matching precedence¶
Nginx does not simply use the first matching location block in
the file — it follows a fixed precedence order regardless of how the
blocks are arranged:
location = /exact/path— exact match, wins immediately if it matches, no further checking.- Prefix matches (
location /api/) — Nginx finds the longest matching prefix among all of them, not the first one written. - Regex matches (
location ~ \.php$or~*for case-insensitive) — checked in the order they appear in the config file; the first regex that matches wins, and regex matches take priority over a plain prefix match (but not over an exact match). - If no regex matches, Nginx falls back to the longest prefix match found in step 2.
This ordering — not file order — is the single most common source of "why is my location block being ignored" confusion, since it's easy to assume Nginx just reads top to bottom like a firewall rule list.
Rate limiting¶
http {
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
}
}
}
limit_req_zone defines a shared-memory zone keyed by
$binary_remote_addr (the client IP) tracking request rate — this is
Nginx's own implementation of the
leaky-bucket-shaped rate limiting
already covered as a system-design case study, applied at the web
server layer instead of an application-level Redis-backed limiter.
rate=10r/s sets the steady-state limit; burst=20 allows up to 20
requests to queue briefly above that rate before being rejected (this
is the token-bucket-style burst allowance from that same case study,
not a hard per-second cutoff); nodelay serves burst-allowance
requests immediately instead of artificially spacing them out to
match the steady rate — without nodelay, requests within the burst
allowance are still accepted but deliberately delayed to smooth the
rate, which is sometimes the desired behavior and sometimes not,
depending on whether smoothing or low-latency-within-budget matters
more for the specific endpoint.
gzip and security headers¶
gzip on;
gzip_types text/plain application/json text/css application/javascript;
gzip_min_length 1024;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
gzip_min_length avoids compressing tiny responses where compression
overhead outweighs the bandwidth saved. The security headers are
cheap, high-value defaults: X-Content-Type-Options: nosniff stops
browsers from guessing content types in a way that can enable certain
attacks; X-Frame-Options: DENY prevents the page from being embedded
in an iframe (clickjacking mitigation); Strict-Transport-Security
tells the browser to only ever connect over HTTPS for this domain
going forward, closing the window for a downgrade attack on
subsequent visits.
TLS termination¶
Nginx commonly terminates TLS at the edge (handles the TLS handshake itself, decrypts incoming requests) and forwards plain HTTP to backend servers on a trusted internal network — moving the CPU cost of encryption to one place and simplifying certificate management to one location instead of every backend instance needing its own certificate.
Common pitfall¶
Assuming limit_req's burst parameter means "allow this many extra
requests per second" rather than "allow a queue of up to this many
requests beyond the steady rate before rejecting" is a common misread
— burst=20 with rate=10r/s doesn't mean 20 extra requests/second
forever, it means a client bursting past 10r/s can have up to 20
requests queued (and, with nodelay, served immediately) before
further excess requests start getting 503'd, and that budget refills
at the steady rate over time, not once per second flatly.