S
Home Application Security ยท Updated 2026-07-21

Nginx Hardening

Nginx is the most widely deployed web server and reverse proxy on the internet, which makes it a high-value, well-studied target. Its default configuration is reasonably sane out of the box, but production deployments accumulate risk over time: permissive location blocks copied from Stack Overflow, missing request limits, self-signed certs left in place "temporarily," and reverse-proxy configurations that trust client-supplied headers they shouldn't. This guide covers hardening Nginx as a web server and reverse proxy, aligned to the CIS Nginx Benchmark and NIST SP 800-53 Rev. 5 control mappings.

Scope: Nginx (open source / OSS) as a standalone web server or reverse proxy on Linux. For broader multi-server guidance (Apache, IIS) and HTTP security header details, see Web Server Hardening; for certificate lifecycle management, see SSL/TLS Certificates.


1. Installation and Minimal Footprint

  • Install from the official nginx.org repository or your distribution's maintained package, not a stale distro-default build โ€” this keeps you on a supported patch cadence.
  • Enable only the modules actually in use. If compiling from source, exclude modules you don't need (--without-http_autoindex_module if directory listing is never required, for example); packaged builds load modules dynamically via load_module โ€” don't load ones you don't use.
  • Keep Nginx current. Check the running version against the latest stable release and subscribe to the nginx security advisories:
nginx -v

2. Run as a Non-Privileged User, Isolate the Process

Nginx's master process starts as root only to bind privileged ports (80/443) and manage worker processes โ€” the workers that actually handle requests should run unprivileged.

# /etc/nginx/nginx.conf
user  nginx nginx;

Confirm no worker process is running as root:

ps aux | grep '[n]ginx: worker process'

systemd Hardening

If Nginx runs under systemd, layer OS-level sandboxing on top of the user directive via a drop-in override:

# /etc/systemd/system/nginx.service.d/override.conf
[Service]
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/log/nginx /var/cache/nginx /var/run
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETUID CAP_SETGID
AmbientCapabilities=CAP_NET_BIND_SERVICE

CAP_NET_BIND_SERVICE lets the master process bind to ports below 1024 without full root; ProtectSystem=strict makes the entire filesystem read-only to the unit except the paths explicitly listed writable. Test thoroughly in staging โ€” an overly strict ReadWritePaths will break config reload or logging silently.


3. Suppress Version and Server Information

# http { } block
server_tokens off;

This removes the Nginx version from the Server response header and from default error pages (Server: nginx instead of Server: nginx/1.25.3), denying attackers an easy CVE-matching fingerprint. It does not remove the header entirely โ€” for that, the headers-more third-party module (more_clear_headers) or a custom build is required.

Also strip upstream application headers that leak stack details when proxying:

proxy_hide_header X-Powered-By;
proxy_hide_header X-AspNet-Version;
fastcgi_hide_header X-Powered-By;

4. TLS Configuration

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    ssl_certificate     /etc/nginx/ssl/example.com.fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    # Protocols -- TLS 1.2 minimum, 1.3 preferred
    ssl_protocols TLSv1.2 TLSv1.3;

    # Strong cipher suites (TLS 1.2 -- 1.3 uses its own fixed, already-strong suite set)
    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;
    ssl_prefer_server_ciphers off;   # let modern clients pick; TLS 1.3 ignores this directive anyway

    # Session resumption
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;         # disable stateful session ticket reuse across restarts

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/nginx/ssl/example.com.chain.pem;
    resolver 1.1.1.1 8.8.8.8 valid=300s;

    # HSTS -- only on a server block you're certain is always HTTPS
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    # Strong Diffie-Hellman group for legacy DHE ciphers (skip if only using ECDHE above)
    ssl_dhparam /etc/nginx/ssl/dhparam.pem;
}

# Redirect all plain HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com;
    return 301 https://$host$request_uri;
}
  • Generate a 2048-bit (minimum) or 4096-bit DH param file if any DHE ciphers remain in scope: openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096. Prefer ECDHE-only cipher lists above and skip this entirely where compatibility allows.
  • Verify the deployed configuration with an external scanner (Qualys SSL Labs, testssl.sh) after every change โ€” cipher/protocol regressions are easy to introduce silently.
  • Set the private key file to 600, owned by the user that reads it at startup (typically root, since Nginx reads certs before dropping privileges):
chmod 600 /etc/nginx/ssl/example.com.key
chown root:root /etc/nginx/ssl/example.com.key

5. HTTP Security Headers

Covered in depth in Web Server Hardening โ€” the Nginx-specific syntax:

add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Content-Security-Policy "default-src 'self'; frame-ancestors 'none'" always;

add_header does not inherit into a nested location block that defines its own add_header directives โ€” repeat every header you need at each level, or centralize them with the third-party headers-more module / an include snippet applied consistently.


6. Request Limits and Denial-of-Service Mitigation

http {
    # Cap request body size (uploads) -- return 413 above this instead of buffering unbounded data
    client_max_body_size 10m;

    # Header/URI size limits -- mitigate abusive or malformed requests
    large_client_header_buffers 4 8k;
    client_header_buffer_size 1k;

    # Timeouts -- mitigate slow-loris style connection exhaustion
    client_body_timeout 10s;
    client_header_timeout 10s;
    send_timeout 10s;
    keepalive_timeout 30s;
    keepalive_requests 1000;

    # Rate limiting -- define zones in http{}, apply in server/location
    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_conn_zone $binary_remote_addr zone=conn_limit:10m;
}

server {
    location / {
        limit_req zone=general burst=20 nodelay;
        limit_conn conn_limit 20;
    }

    location /login {
        limit_req zone=login burst=5 nodelay;
    }
}
  • Tune client_max_body_size per application โ€” a global default that's too generous on an API that never accepts uploads is unnecessary exposed surface.
  • Apply a tighter limit_req zone to authentication, password-reset, and other abuse-sensitive endpoints than to general traffic โ€” credential stuffing and enumeration attacks concentrate there.
  • limit_req/limit_conn are per-worker-process in older Nginx versions unless zone memory is shared correctly (it is, by the zone= directive above) โ€” verify behavior under load rather than assuming the configured rate is exact under high concurrency.

HTTP/2 Rapid Reset (CVE-2023-44487) Mitigation

http {
    http2_max_concurrent_streams 128;   # default is already reasonable; lower if abuse observed
    keepalive_requests 1000;            # recycling connections limits reset-based amplification
}

Ensure Nginx is patched to a version with the upstream fix (1.25.3+ / 1.24.0 with the backported patch) โ€” configuration tuning alone does not fully close this class of issue.


7. Restrict HTTP Methods

location / {
    limit_except GET POST HEAD {
        deny all;
    }
}

Explicitly deny TRACE (cross-site tracing / header disclosure) and any WebDAV methods (PUT, DELETE, PROPFIND, MKCOL) not intentionally in use โ€” limit_except denies everything not listed for that location.


8. Directory Listing and Sensitive File Protection

# Never enable in production unless a specific directory intentionally serves an index
autoindex off;

# Deny access to dotfiles and common sensitive artifacts
location ~ /\. {
    deny all;
    return 404;
}

location ~* \.(git|env|bak|backup|old|swp|log|sql|conf)$ {
    deny all;
    return 404;
}

# Deny access to version control and dependency directories if ever served from webroot
location ~ /(\.git|\.svn|node_modules|vendor)/ {
    deny all;
    return 404;
}

Return 404 rather than 403 for these โ€” a 403 confirms the resource exists but is forbidden, which is itself minor information disclosure to an attacker probing for .git/.env files.


9. Reverse Proxy Hardening

Trust Client IP Headers Only From Known Proxies

If Nginx sits behind a load balancer or CDN, $remote_addr is the LB's IP, not the client's โ€” but blindly trusting a client-supplied X-Forwarded-For header lets any user spoof their apparent source IP for logging, rate limiting, or WAF allow-lists. Use the realip module scoped to your actual upstream infrastructure only:

# Only trust X-Forwarded-For from your known load balancer / CDN ranges
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 173.245.48.0/20;   # example: Cloudflare range
real_ip_header X-Forwarded-For;
real_ip_recursive on;

Never set set_real_ip_from 0.0.0.0/0 โ€” that trusts the header from anyone, defeating the purpose entirely.

Proxying Upstream Safely

location / {
    proxy_pass http://upstream_app;
    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;

    proxy_connect_timeout 5s;
    proxy_read_timeout 30s;
    proxy_send_timeout 30s;

    proxy_hide_header X-Powered-By;
    proxy_redirect off;
}
  • SSRF via proxy misconfiguration: never construct proxy_pass targets from unsanitized user input (a dynamic $uri/$arg_ fed into the upstream host/port). This is a common source of internal-network pivot in reverse-proxy setups.
  • Set explicit, conservative proxy_*_timeout values so a slow or hung upstream can't tie up worker connections indefinitely.
  • If proxying to cloud-hosted upstreams, block requests to cloud metadata endpoints (169.254.169.254) at the network layer in addition to application-layer controls โ€” a reverse proxy misconfiguration should not be a path to instance credential theft.

10. Logging

log_format security '$remote_addr - $remote_user [$time_local] '
                     '"$request" $status $body_bytes_sent '
                     '"$http_referer" "$http_user_agent" '
                     '"$http_x_forwarded_for" rt=$request_time';

access_log /var/log/nginx/access.log security;
error_log  /var/log/nginx/error.log warn;
  • Include $http_x_forwarded_for and request timing ($request_time) โ€” both are valuable for incident investigation and abuse detection.
  • Never log request bodies, Authorization headers, or session cookies in custom log formats โ€” access logs are frequently less tightly access-controlled than the application itself, and become a secondary credential-exposure surface if they capture sensitive data.
  • Forward access.log/error.log to a central SIEM. Alert on request-rate spikes against a single path, repeated 401/403/404 bursts from one source (probing), and sudden appearance of TRACE/PROPFIND/other unexpected methods.
  • Rotate and retain per your compliance requirement (logrotate is standard on most distros' Nginx packages) โ€” verify retention actually meets policy rather than assuming the default is sufficient.

11. Configuration and File Permissions

chmod 644 /etc/nginx/nginx.conf
chown root:root /etc/nginx/nginx.conf /etc/nginx/conf.d/*.conf
chmod 600 /etc/nginx/ssl/*.key
  • Keep TLS private keys unreadable to anyone but root/the process that needs them at startup.
  • Don't store upstream API keys or shared secrets in plaintext proxy_set_header directives inside version-controlled config โ€” use an include file with restricted permissions outside the repo, or inject via a secrets manager into a template at deploy time.
  • Validate configuration syntax before every reload, and prefer reload (graceful) over restart (drops connections):
nginx -t && systemctl reload nginx

12. Web Application Firewall

For applications facing the public internet, terminate common injection, XSS, and scanner traffic in front of the application with ModSecurity (libmodsecurity + the OWASP Core Rule Set) or NAXSI. See Web Application Firewall for deployment guidance and tuning to avoid false-positive lockouts on legitimate traffic.


Hardening Checklist

Control Priority Verify With
Worker processes run as unprivileged user Critical ps aux \| grep 'nginx: worker'
server_tokens off Medium curl -I response Server header
TLS 1.2/1.3 only, strong ciphers Critical testssl.sh / Qualys SSL Labs scan
HSTS enabled on all HTTPS server blocks High Response headers
OCSP stapling enabled Medium openssl s_client -status
Private key files 600, root-owned Critical stat /etc/nginx/ssl/*.key
client_max_body_size set explicitly Medium nginx -T \| grep client_max_body_size
Timeouts configured (slow-loris mitigation) High nginx -T \| grep timeout
Rate limiting on general and auth endpoints High nginx -T \| grep limit_req
Disallowed HTTP methods denied Medium curl -X TRACE / curl -X PROPFIND
autoindex off everywhere High nginx -T \| grep autoindex
Dotfiles / .git / .env denied Critical curl a known dotfile path, expect 404
X-Forwarded-For trusted only from known proxy ranges Critical nginx -T \| grep set_real_ip_from
No SSRF-prone dynamic proxy_pass targets Critical Config review of all proxy_pass directives
Access/error logs forwarded to SIEM High SIEM ingestion config
Sensitive data excluded from log format High nginx -T \| grep log_format
Config files not world-writable High stat /etc/nginx/nginx.conf
Nginx version current / patched High nginx -v vs. latest stable
WAF (ModSecurity/NAXSI) in front of public apps High Module/rule set presence

NIST SP 800-53 Rev. 5 Control Mapping

Control Title How Nginx hardening addresses it
AC-4 Information Flow Enforcement realip trust scoping, method restrictions, sensitive-path denial
AC-6 Least Privilege Unprivileged worker processes, systemd capability restriction
SC-5 Denial of Service Protection limit_req/limit_conn, timeouts, HTTP/2 stream limits
SC-7 Boundary Protection Reverse-proxy hardening, SSRF/metadata-endpoint mitigation
SC-8 Transmission Confidentiality/Integrity TLS 1.2/1.3-only, strong cipher suites, HSTS
SC-13 Cryptographic Protection OCSP stapling, session ticket rotation
SC-28 Protection of Information at Rest Restricted key file permissions
CM-6 Configuration Settings server_tokens off, denied methods, autoindex off
CM-7 Least Functionality Minimal module set, WAF-scoped rule enforcement
AU-2 / AU-3 Audit Events / Content of Audit Records Security-focused log_format with client and timing data
AU-6 Audit Review, Analysis, Reporting SIEM forwarding and alerting on abuse patterns
SI-4 System Monitoring WAF (ModSecurity/NAXSI), rate-limit and log-based alerting

References

The Security Architecture Site โ€” for internal reference use. Back to contents