SERVER SECURITY GUIDE

Nginx Security Best Practices

Hardening nginx means configuring TLS correctly, sending the security headers browsers expect, limiting request rates and sizes, hiding version information, and logging enough detail to investigate incidents later. None of these steps require replacing nginx or buying additional software: they are configuration changes you can make directly in your server blocks, most of them in under an hour combined.

Quick win

  • Add server_tokens off; to your http {} block and reload nginx.
  • Add the five core security headers (see below) to your main server block.
  • Set client_max_body_size to the smallest value your application actually needs.
  • Add a limit_req_zone rule for your login page or admin path.
  • Run nginx -t to validate syntax, then systemctl reload nginx to apply changes without dropping connections.

TLS and SSL configuration in nginx

A secure nginx TLS configuration supports only TLS 1.2 and TLS 1.3, disables legacy protocols and weak ciphers, and enables OCSP stapling. Getting this right protects every other layer of the stack, because a compromised transport channel undermines authentication, headers, and everything else.

TLS 1.0 and TLS 1.1 carry known weaknesses and should not be offered. The Mozilla SSL Configuration Generator (ssl-config.mozilla.org) is the most practical way to generate a configuration that matches your OpenSSL version and desired compatibility level. Its Intermediate profile, which supports TLS 1.2 and 1.3 with strong ciphers while remaining broadly compatible with real-world browsers and API clients, is the right default for most public sites. The Modern profile restricts connections to TLS 1.3 only and is appropriate when you control every client that connects, for example an internal API.

A representative intermediate-style configuration looks like this:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m;
ssl_session_tickets off;

# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;

Always generate the cipher list for your exact nginx and OpenSSL version through the Mozilla generator rather than copying a cipher string from an old blog post, since recommended cipher suites change as weaknesses are found. After deploying, verify the result with an independent scanner (such as SSL Labs) rather than assuming the configuration file alone guarantees a good grade, since intermediate certificates, HSTS headers, and chain order also affect the outcome.

In the news: if your certificates come from Let's Encrypt, its certificate lifetimes are shrinking from 90 days toward 45 and eventually fewer, which can break renewal automation that assumes a fixed interval.

Nginx security headers

Security headers tell the browser to enforce protections nginx cannot enforce on its own, such as blocking clickjacking, MIME-sniffing, and mixed-content loading. Nginx sends none of the following headers by default, so every one of them has to be added explicitly.

The OWASP Secure Headers Project (a documentation project maintained by OWASP that lists which headers to add and which to remove) recommends this baseline set, which you can add inside your server {} block:

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
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=(), camera=(), microphone=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Cross-Origin-Resource-Policy "same-site" always;

A few notes on using these correctly. Strict-Transport-Security (HSTS) tells browsers to only ever connect over HTTPS for the specified duration; do not enable preload until you are certain every subdomain supports HTTPS, since preload lists are slow to reverse. Content-Security-Policy is intentionally left out of the baseline snippet above because, per the OWASP guidance, it requires customization to your specific site's scripts, styles, and third-party resources rather than a one-size-fits-all value; a misconfigured CSP can break your own site's functionality, so build it incrementally in report-only mode first (Content-Security-Policy-Report-Only) before enforcing it. The always parameter ensures headers are sent on error responses too, not just 200 OK responses, which matters because attackers frequently probe error pages.

Not sure which headers and TLS settings your server is actually running?

Get a free, read-only security check that reviews your current configuration, or talk to a security engineer about ongoing monitoring and managed hardening.

Get your free server check Talk to a security engineer

Rate limiting in nginx

Rate limiting caps how many requests a single client can send in a given time window, which blunts brute-force login attempts, credential stuffing, and basic denial-of-service traffic before it reaches your application. Nginx implements this natively through the ngx_http_limit_req_module, using a leaky-bucket algorithm, according to the official nginx documentation.

A typical setup defines a shared-memory zone keyed by client IP, then applies it to the endpoints attackers actually target, such as login forms and search:

http {
    limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;

    server {
        location /login {
            limit_req zone=login burst=5 nodelay;
            proxy_pass http://app_backend;
        }
    }
}

The rate parameter sets the sustained allowed rate (here, 5 requests per minute per IP). The burst parameter allows a short queue of extra requests above that rate before nginx starts rejecting them, and nodelay processes queued burst requests immediately rather than artificially spacing them out, which keeps the experience smooth for legitimate users who happen to click twice. Apply strict limits narrowly, on login, password reset, search, and API endpoints, rather than site-wide, since an overly aggressive global limit will reject real visitors during normal traffic spikes. Nginx also supports limit_req_dry_run on; to log what would have been limited without actually enforcing it, which is useful for tuning thresholds before switching them on in production.

Rate limiting at the web server layer is one part of a broader defense against volumetric and application-layer abuse; see our DDoS protection guide for small businesses for the layers above and below nginx, including upstream filtering and firewall-level controls.

Hiding nginx server tokens and version details

By default nginx sends its exact version number in the Server response header and on default error pages, which lets automated scanners match your server against known vulnerabilities for that specific version. Setting server_tokens off; in the http {} block removes the version number from both locations.

http {
    server_tokens off;
}

This is not a substitute for patching; a determined attacker can often still fingerprint nginx by behavior. But it removes a free, zero-effort signal that automated vulnerability scanners rely on, and it takes one line and a reload to enable. For a more complete rewrite of default error pages and further reduction of information disclosure, dedicated hardening guides such as the Gixy nginx security analyzer project cover additional header-stripping techniques, but server_tokens off covers the standard case for the vast majority of sites. Hiding the version number does not help against a specific, disclosed vulnerability like NGINX Rift, where the fix is a version upgrade, not a configuration tweak.

Limiting request size and unused methods in nginx

Setting a request body size limit and restricting HTTP methods closes off resource-exhaustion attacks and reduces what an attacker can even attempt against your application. Both are single-directive changes.

client_max_body_size controls the largest request body nginx will accept before returning a 413 error. Leaving it at nginx's default, or raising it without a specific reason, allows attackers (or misbehaving clients) to send oversized uploads that consume disk, memory, and worker time:

server {
    client_max_body_size 1m;  # raise only for endpoints that need uploads
}

location /uploads/ {
    client_max_body_size 20m;
}

Set the limit as low as your application genuinely needs at the server level, and only override it upward inside specific location blocks that actually handle file uploads. This scopes the exposure to the one path that needs it instead of the whole site.

Similarly, if your site or API only ever needs GET, HEAD, and POST, you can explicitly reject other methods:

location / {
    if ($request_method !~ ^(GET|HEAD|POST)$) {
        return 405;
    }
}

Note that nginx documentation generally advises against using if inside location blocks except for a narrow set of cases like this one, because if has known surprising interactions with other directives; keep such blocks simple and test them after any change with nginx -t.

Disabling unused nginx modules

Every compiled-in nginx module is additional code that can contain bugs or be misconfigured, so removing modules you don't use reduces your attack surface even if none of them are currently exploitable. This applies at build time, not just at the configuration level.

If you build nginx from source or use a package with a configurable module set, disable modules you are not using, for example WebDAV (ngx_http_dav_module), autoindex if directory listing isn't needed, or the SSI module if you don't use server-side includes. Check your compiled module list with:

nginx -V 2>&1 | tr ' ' '\n' | grep module

At the configuration level, the same principle applies to autoindex: leaving directory listing enabled on a path that shouldn't be browsable can expose file names and structure you didn't intend to publish. Explicitly set autoindex off; (the nginx default) and confirm it in your configuration rather than assuming it's off, especially on any location block serving static files or uploads.

Nginx log format for security visibility

A security-useful nginx log format captures client IP, request time, status code, and processing time, not just the bare minimum needed to count hits. The default combined log format is a reasonable starting point but omits timing data that helps distinguish normal traffic from automated abuse or slow-loris style attacks.

A more security-aware custom format adds $request_time (total time nginx spent handling the request) alongside the standard fields:

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

access_log /var/log/nginx/access.log security;

$request_time is worth adding to essentially every deployment because it shows the amount of time nginx took to process the request to millisecond precision, which helps identify both performance problems and certain attack patterns, such as a flood of requests that are individually cheap but collectively overwhelming. Logs are only useful if something actually reviews them; on their own, access and error logs sitting on disk do not detect anything. Our companion guide on server monitoring and log analysis covers how to turn this log data into actual early-warning signal, including centralizing it and separating real incidents from routine noise.

For the full picture of where nginx hardening fits into securing an entire VPS, from SSH and the firewall down to backups, see the VPS security checklist. If nginx sits behind a firewall you haven't reviewed recently, our Linux firewall guide covers the layer immediately below it.

One common nginx mistake is serving files that were never meant to be public. The guide to exposed .env and .git files shows how to test for them and includes the nginx location rules that block them without breaking Let's Encrypt renewal.

Directive references: limit_req for request rate limiting and ssl_protocols for TLS versions, each with accepted values, defaults and a test.

Reference pages: HSTS and Reverse proxy.

Frequently asked questions

What is the single most important nginx security setting?

There is no single setting that matters most, but a modern TLS configuration (TLS 1.2 and 1.3 only, strong ciphers) combined with server_tokens off and a rate limiting rule for login or search endpoints covers the highest-impact, lowest-effort ground.

Does server_tokens off actually stop attacks?

By itself, no. It removes the nginx version number from the Server header and default error pages, which makes it slightly harder for automated scanners to fingerprint your exact version and match it to known vulnerabilities, but it does not patch anything or block traffic on its own.

Should I use the Mozilla Modern or Intermediate TLS profile?

For most public-facing sites, the Intermediate profile from the Mozilla SSL Configuration Generator is the right default because it supports TLS 1.2 and 1.3 with strong ciphers while remaining compatible with the browsers and API clients your visitors actually use. Modern-only (TLS 1.3 exclusively) is appropriate when you control every client connecting to the server.

How do I rate limit nginx without breaking legitimate traffic?

Use the ngx_http_limit_req_module with a conservative rate on sensitive endpoints like login and search, and set a burst value with nodelay so that short, normal traffic spikes are absorbed instead of rejected outright. Apply the strictest limits to endpoints attackers actually target, not to your entire site.

Do I need a Web Application Firewall (WAF) in addition to hardening nginx?

Hardening nginx configuration and running a WAF address different layers. Configuration hardening reduces the attack surface and information leakage of the web server itself; a WAF inspects request content for known attack patterns. Many small businesses start with hardening plus monitoring, and add a WAF or managed protection once traffic and risk justify it.

What is the maximum safe value for client_max_body_size?

There is no universal number. The safest approach is to set client_max_body_size as low as your application legitimately needs (for example 1m for a form-only site, or higher for file uploads) rather than leaving it at nginx's default or raising it indefinitely, since oversized request bodies are a common resource-exhaustion and abuse vector.

How often should I revisit my nginx security configuration?

Review it whenever nginx is upgraded, whenever new subdomains or applications are added behind it, and at minimum every few months, because TLS cipher recommendations and header guidance from sources like Mozilla and OWASP change over time as new weaknesses are found.

Not sure where your server stands?

Get a free, read-only security check, or talk to a security engineer about managed protection.

Get your free server check Talk to a security engineer