.hero Opens in a new tab

How to Secure Nginx: A Step-by-Step Hardening Guide

2026-09-21
Shield icon between code brackets on purple, illustrating how to secure Nginx

Nginx ships to be easy to run, not hard to attack. A default install serves traffic fine. It also announces its exact version to anyone who asks, permits TLS protocols from the last decade, and leaves security headers unset. It powers a huge share of the web, which makes it a standing target. Every one of those defaults is something an attacker checks for first. This nginx security guide is a build sequence, not a theory lesson. Work through it in order and you finish with a hardened configuration you can actually trust. Each step ends in the directive you paste into your config. Reload after each change and test before moving on.

Keep Nginx Patched and Track Nginx CVEs

Every hardening directive below assumes the software underneath it is current, the same rule that applies when you harden Apache. A perfectly tuned config on a vulnerable Nginx build is still vulnerable, because a flaw in the binary is not something a directive can close. Patching is the floor the rest of nginx hardening stands on.

Keep Nginx current through your package manager, and know your running version so you can tell whether a newly published CVE actually affects you:

nginx -v

If you run the official Nginx repository rather than your distribution’s packages, you get newer releases sooner, which matters when a security fix lands. Whichever you use, the goal is a short gap between a patch being published and it being installed. That gap is the window an attacker works in.

The harder part is knowing which CVEs matter. Most published Nginx vulnerabilities will not apply to your version or your enabled modules, and chasing all of them burns time you do not have. CVE Watch tracks Nginx CVEs and flags which already have a functional exploit through Exploit-DB. That lets you tell a genuine fire from background noise and patch the one that threatens your version first.

Hide the Version Banner

By default, Nginx puts its exact version in the Server response header and on its error pages. That hands an attacker the one piece of information they need to look up which exploits apply to your specific build. Turning it off does not fix a vulnerability, but it removes the free reconnaissance.

In the http block:

server_tokens off;

That drops the version number from headers and error pages, leaving only the generic nginx name. This is defense by obscurity, and on its own it protects nothing, but it raises the effort for opportunistic scanners that filter targets by version. Combined with the rest of this guide, it is a free line worth setting. It is a standard item in any list of nginx security best practices for exactly that reason.

Enforce HTTPS Properly

Padlock icon between code brackets on purple, illustrating how to enforce HTTPS properly on Nginx

Serving over HTTPS is not the same as enforcing it. A secure Nginx configuration redirects all plaintext traffic to TLS, and it only offers protocol versions and ciphers that are still sound. TLS 1.0 and 1.1 are deprecated and should be off; TLS 1.2 is the floor and 1.3 is preferred.

In your TLS server block:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384';

And force plaintext requests up to HTTPS with a redirect in the port 80 server block:

return 301 https://$host$request_uri;

Two things separate a secure Nginx configuration from one that merely has TLS switched on. First, the redirect has to be unconditional, so there is no plaintext path left open for a downgrade. Second, the cipher list should be reviewed against current guidance periodically, because a cipher considered strong three years ago may be deprecated now. TLS is the one area of this guide where a good setting today can quietly become a weak one. It belongs on the recurring review list rather than the set-once list.

Set the Security Headers

Nginx sends no security headers unless you add them, and these are the cheapest hardening you will do all day. Each one instructs the browser to refuse a class of attack. HSTS forces HTTPS on future visits. The frame and content-type headers block clickjacking and MIME sniffing, and a referrer policy limits what you leak to other sites.

In the server or http block:

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;
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;

A Content-Security-Policy header is the strongest of the set, but it has to be written for your specific application. Add it deliberately once you know what your pages legitimately load. These nginx security headers are among the highest-value lines in the whole config. Note the always flag on each: without it, Nginx omits the header on error responses like 500s, which are exactly the responses an attacker is most likely to probe. Once set, verify them from a browser’s developer tools or with curl -I against your site. A header you assume is present and one you have confirmed are different things.

Disable Unused Modules and Unsafe HTTP Methods

Every module compiled in and every HTTP method allowed is attack surface you may not be using. Most web applications need only GET, POST, and HEAD. Methods like TRACE, DELETE, and PUT are frequently enabled by default and rarely needed, and each one is a capability an attacker can try to abuse.

Restrict methods in the server or location block:

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

If you build Nginx yourself, compile out modules you do not use rather than leaving them present. Less code running is less code to exploit. This is a step people skip because the defaults work. But to properly harden Nginx server deployments, you reduce what is enabled to only what the application calls, module by module and method by method.

Run as a Non-Privileged User

Hexagonal node network between code brackets, illustrating running Nginx as a non-privileged user

Nginx worker processes should never run as root. If a worker is compromised, the account it runs under is the account the attacker inherits. That account should be able to do as little as possible. Most distributions set this up correctly, but a hardening pass confirms it rather than assuming.

In the main context of nginx.conf:

user www-data;

Use the low-privilege account your distribution provides (www-data on Debian and Ubuntu, nginx on RHEL and CentOS), and confirm that account owns no more of the filesystem than it needs. This is one of the highest-value steps when you harden an Nginx server. The gap between a worker that only reads web files and one that rewrites your application comes down to what its user account may touch. Web root directories should be owned by root and only readable by the Nginx user, not writable by it. Then a compromised worker cannot modify the very files it serves.

Add Rate Limiting and Connection Limits

Without limits, a single client can exhaust your server with request floods or slow-drip connection attacks. Nginx can cap both the request rate and the number of concurrent connections per client, which blunts brute-force attempts, credential stuffing, and basic denial-of-service pressure.

Define a rate limit zone in the http block and apply it to a sensitive location such as a login endpoint:

limit_req_zone $binary_remote_addr zone=one:10m rate=10r/s;
location /login {
limit_req zone=one burst=20 nodelay;
}

The limit_req directive above caps request rate; limit_conn caps simultaneous connections from one address, which is what defends against slow-connection attacks that tie up workers without sending much traffic. Layer both for meaningful protection. Tune the rate to your real traffic. Too strict and you block legitimate users. Too loose and the limit does nothing. Base it on what normal use actually looks like, not a number copied from a guide.

Restrict Access by IP and Lock Down Sensitive Locations

Some parts of a site should never be public. Admin panels, status endpoints, and hidden files like .git or .env are common footholds, and Nginx can refuse them before the request reaches your application. Restrict administrative paths to known networks, and block access to dotfiles outright.

Limit an admin location to trusted addresses:

location /admin {
allow 203.0.113.0/24;
deny all;
}

And deny hidden files everywhere:

location ~ /\. {
deny all;
}

That single dotfile rule closes off an entire category of accidental exposure, from a committed .git directory to a stray .env full of credentials. Enforcing these restrictions at the Nginx layer matters because it stops the request before it reaches your application code. An access rule that lives only inside the app is one a bug in the app can bypass. Nginx refuses the request outright, no matter what the application would have done with it.

Keep Your Config Hardened Over Time

A config hardened today drifts tomorrow. A deploy overwrites a setting, someone adds a location block under deadline pressure and forgets the access rules, a package update resets a default. Nginx hardening is not a one-time task, it is a state you maintain, and the way you maintain it is by checking rather than assuming.

Validate the config syntax after every change, before you reload, so a typo never takes the server down:

nginx -t

That catches syntax errors, but it does not tell you whether the config is still secure. For that you need to measure it against a standard rather than eyeball it. The Panopsys Hardening module audits Nginx configuration against established hardening baselines as part of a broader stack audit, and the free plan includes one hardening audit. It uses a readable script you run yourself, agentless, with nothing to install. It returns each check as compliant, non-compliant, or not applicable, so drift surfaces as a finding rather than as a breach. Paired with CVE Watch tracking the Nginx vulnerabilities in your running version, that closes both halves: the configuration stays hardened, and the software behind it stays watched. Nginx security is not a state you reach once and file away. It is the combination of a hardened config and a patched binary, held together over time. The two soft spots this guide closes, configuration drift and unpatched CVEs, are the two that quietly reopen the door after everyone has moved on.

Categories
Share post
Related posts
Shield icon inside an octagon on a soft gradient, representing the configuration assessment process

What Is Configuration Assessment? Process, Benefits and Best Practices

Line icon of a hexagonal node inside a scanning frame on purple, illustrating CIS configuration benchmarks

What Is CIS Configuration? Benchmarks, Controls and How to Harden Your Systems