Web Server Security and Hardening Guide for Nginx
Harden NGINX step by step with patching, TLS, access controls, rate limits, security headers, verification, and troubleshooting.

NGINX hardening starts with patching the server, limiting what it exposes, enforcing modern TLS, protecting administrative paths, controlling abusive traffic, and verifying every change from an external client. A secure configuration is a process: inventory the deployment, apply the controls that fit its trust boundaries, test the effective behavior, and review it whenever the software or application changes.
At research time, the official NGINX download page lists 1.30.5 as the stable release and 1.31.6 as the mainline release. Check the NGINX security advisories for fixed versions for each CVE before selecting a branch. Release numbers change, so treat the advisory page as the authority.
1. Inventory NGINX before changing it
Write down the installed package and version, enabled modules, listening addresses and ports, virtual hosts, upstream services, administrative URLs, authentication layers, trusted proxies, certificate locations, and log destinations. This inventory identifies the trust boundaries your configuration must enforce.
nginx -v
nginx -V 2>&1
sudo nginx -T
sudo ss -lntup
Review the output for accidental listeners, obsolete virtual hosts, debug modules, proxy targets that should not be public, and locations serving source repositories, backups, environment files, or private keys. Compare the installed version with the download page and security advisories. Upgrade through your operating system’s signed package process where available, then retain the package verification records used by your organization.
2. Enforce HTTPS and modern TLS
Redirect cleartext HTTP and serve the application from a TLS virtual host. NGINX’s HTTPS guidance uses TLS 1.2 and TLS 1.3:

server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/tls/fullchain.pem;
ssl_certificate_key /etc/nginx/tls/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
The private key is a secure entity. Store it in a file with restricted access while keeping it readable by the NGINX master process, as described in the NGINX HTTPS configuration guide. Check ownership and mode after every certificate deployment:
sudo chown root:root /etc/nginx/tls/privkey.pem
sudo chmod 600 /etc/nginx/tls/privkey.pem
sudo nginx -t
sudo systemctl reload nginx
Use a complete certificate chain, renew certificates before expiry, and test both the redirect and the final HTTPS response from outside the host. Add HSTS only after every required hostname works over HTTPS; HSTS can make recovery from a mistaken HTTP deployment harder.
3. Remove unnecessary exposure
Bind NGINX only to addresses that must accept traffic. Close unused ports at the host firewall and at any cloud security group. Separate public, private, and administration listeners where possible. Never publish files that contain credentials, source history, deployment archives, or configuration.
location ~* /(?:\.git|\.svn|\.hg|backup|backups|\.env|secrets)(?:/|$) {
return 404;
}
location ~* \.(?:bak|conf|dist|fla|ini|log|old|orig|psd|sh|sql|swp|tmp)$ {
deny all;
return 404;
}
Use explicit location blocks for health checks, static assets, APIs, and administrator paths. Avoid a broad fallback that accidentally serves the filesystem. Set the document root to the application’s public directory rather than its repository root.
4. Authenticate and authorize sensitive paths
Choose controls according to the trust model. NGINX supports Basic Authentication, subrequest authentication, IP restrictions, connection limits, and request-rate limits. NGINX Plus adds documented JWT, OpenID Connect, and dynamic denylisting capabilities. Authentication at the reverse proxy does not replace application authorization: the application still needs to check the user’s role and resource permissions.
location /admin/ {
auth_basic 'Restricted administration';
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://app_backend;
}
location /internal/ {
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
proxy_pass http://app_backend;
}
Protect the password file with root ownership and restrictive permissions. If NGINX is behind a load balancer, configure trusted proxy addresses before using the forwarded client address for IP rules. Otherwise, an attacker may supply a forged header or all users may appear to come from the proxy.
5. Rate-limit requests and concurrent connections
NGINX rate limiting can help prevent denial-of-service traffic and keep an upstream from being overwhelmed. Define separate policies for login, password reset, expensive API calls, static files, and health endpoints. A documented example uses a 10 MB shared zone and a rate of 1 request per second; these values are examples, not universal recommendations.
http {
limit_req_zone $binary_remote_addr zone=login_per_ip:10m rate=1r/s;
limit_req_zone $binary_remote_addr zone=api_per_ip:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
server {
location = /login {
limit_req zone=login_per_ip burst=5 nodelay;
limit_conn conn_per_ip 10;
proxy_pass http://app_backend;
}
location /api/ {
limit_req zone=api_per_ip burst=20;
proxy_pass http://app_backend;
}
}
}
Choose the key carefully. Per-IP limits can punish users behind corporate NAT, mobile carriers, or shared proxies. Per-account limits require an authenticated identity and usually belong in the application. Start in a monitored mode, inspect 429 responses and logs, then tune bursts and limits using real traffic. Exempt trusted health checks only when their source is strongly restricted.
6. Add security response headers
Response headers act as browser controls. The OWASP Secure Headers Project documents their purpose and behavior. Add a policy that matches the application instead of copying a restrictive policy that breaks scripts, frames, or integrations.
server {
add_header X-Content-Type-Options 'nosniff' always;
add_header Referrer-Policy 'strict-origin-when-cross-origin' always;
add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
add_header Strict-Transport-Security 'max-age=31536000' always;
add_header X-Frame-Options 'SAMEORIGIN' always;
}
Test the Content-Security-Policy in report-only mode or against a staging copy before enforcement. Account for analytics, payment frames, web fonts, workers, and image hosts. Use either a framing policy that the application supports or omit it until you have confirmed legitimate embedding requirements. Do not assume that headers applied in one location appear on error responses or redirects; the always parameter is useful for consistent coverage.
7. Bound request sizes, timeouts, and proxy behavior
Resource limits reduce the impact of oversized uploads and slow clients. Set them per endpoint instead of using one value for every request.
server {
client_max_body_size 10m;
client_header_timeout 10s;
client_body_timeout 30s;
send_timeout 30s;
location /upload/ {
client_max_body_size 50m;
proxy_request_buffering on;
proxy_read_timeout 60s;
proxy_pass http://app_backend;
}
location /api/ {
proxy_connect_timeout 5s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
proxy_pass http://app_backend;
}
}
For upstream HTTPS, configure certificate validation and an explicit trusted CA rather than silently accepting any certificate. Forward only the headers the application needs. Be deliberate about buffering, retries, and maximum response sizes for endpoints that return user-controlled data.
8. Log, monitor, and review
Log authentication failures, rejected methods, rate-limit events, upstream errors, suspicious paths, and configuration reloads. Send logs to a protected destination with access controls and retention suitable for incident response. Alert on repeated login failures, sudden 4xx or 5xx changes, unexpected listeners, certificate-expiry windows, and configuration changes outside the deployment process.
9. Verify every hardening change
- Run
sudo nginx -tbefore every reload. - Reload only after a successful syntax check:
sudo systemctl reload nginx. - From an external client, check redirects and headers:
curl -I http://example.comandcurl -Ik https://example.com. - Exercise authenticated, unauthenticated, allowed, and denied paths.
- Send enough requests to confirm the intended 429 behavior without locking out shared-NAT users.
- Confirm that only intended ports are reachable and that logs record the expected events.
10. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
nginx -t reports an unknown directive |
The directive’s module is absent or the syntax is invalid. | Check nginx -V, install the required package/module, and compare spelling and context with the official documentation. |
| Reload fails after certificate renewal | Unreadable key, incomplete chain, or wrong path. | Check file ownership and mode, verify the chain, then run nginx -t before retrying. |
| Clients still reach HTTP | The request hits another listener or virtual host. | Inspect nginx -T, DNS, load balancer listeners, and firewall rules; test with the intended Host header. |
| All users receive 429 responses | Rate limiting keys on a shared proxy address or limits are too low. | Fix trusted proxy handling, choose a suitable key, and tune rate and burst values from logs. |
| CSP breaks the application | The policy omits a required script, frame, font, or image origin. | Use report-only testing, inventory legitimate dependencies, and add only the sources the application needs. |
| Admin access is denied unexpectedly | The client address is evaluated as the load balancer’s address or an allow rule is too narrow. | Configure trusted proxy addresses and verify the effective client IP in logs. |
| Users see stale or unexpected content | Caching, proxy buffering, or an upstream retry obscures an update. | Review cache keys and TTLs, bypass caching for personalized paths, and inspect upstream response headers. |
11. Performance, reliability, and cost considerations
Hardening controls consume resources when they inspect requests, maintain shared zones, write logs, or proxy large bodies. Keep policies scoped to the paths that need them, size shared-memory zones for the number of keys you expect, and avoid verbose logs containing secrets. Rate limits can protect upstream capacity, but an overly strict policy becomes an availability problem for legitimate users.
For reliability, stage configuration changes, keep the previous known-good configuration, and reload rather than stopping the service. Certificate renewal should be automated with a validation step and a monitored expiry deadline. Review firewall rules, package updates, headers, authentication paths, and limit behavior on a recurring schedule. There is no universal throughput penalty or breach-reduction percentage that can be applied to every NGINX deployment; measure your own traffic and failure modes.
12. Or skip the browser setup
If your hardening work includes collecting screenshots of public pages, documenting redirects, or checking externally visible headers, ScreenshotNeo provides a single website screenshot API request. It accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers.

See the ScreenshotNeo API documentation for all options, including full-page capture, element selectors, custom headers and cookies, waits, blocking rules, caching, signed links, asynchronous jobs, bulk capture, PDFs, and usage reporting.
curl -G 'https://api.screenshotneo.com/v1/shot' \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
import requests
r = requests.get('https://api.screenshotneo.com/v1/shot', params={'access_key': 'YOUR_API_KEY', 'url': 'https://example.com'}, timeout=90)
r.raise_for_status()
open('shot.webp', 'wb').write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await Bun.write('shot.webp', data);
ScreenshotNeo also has an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots each month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Should I use stable or mainline NGINX?
Start with the branch your operating system supports and compare it with the security-advisory fixed versions. Security fixes take priority over a preference for either branch.
Is TLS 1.2 still required?
Use TLS 1.2 and TLS 1.3 as shown in NGINX’s HTTPS example unless your compatibility policy explicitly permits a narrower set.
Where should authentication live?
Use NGINX for a coarse boundary around administrative or internal paths, and keep fine-grained authorization in the application.
How often should this configuration be reviewed?
Review after NGINX, operating-system, certificate, identity, proxy, or application changes, and on a recurring schedule that matches your incident-response requirements.


