Harden Nginx on Ubuntu: Headers, TLS & Limits 2026
Out of the box, Nginx is fast and stable but tells the world more than it should and accepts more abuse than it needs to. It advertises its exact version in every response header, negotiates TLS protocols that should have been retired years ago, and lets a single misbehaving client hammer an expensive endpoint as hard as it likes. None of that requires a rewrite to fix — a handful of directives turns a default install into a server that gives attackers less to work with. This guide hardens Nginx on Ubuntu across four fronts: version disclosure, security headers, TLS policy, and request rate limiting — running nginx -t after every change so a typo never takes the site down.
server_tokens off to stop leaking the version, a small set of add_header security headers, ssl_protocols TLSv1.2 TLSv1.3 to drop legacy TLS, and a limit_req_zone to throttle floods. Test with nginx -t before every reload — an invalid config that gets reloaded is an outage, while an invalid config that fails nginx -t is a non-event.Edit the Right File
Site-wide settings belong in the http block of /etc/nginx/nginx.conf; per-site settings belong in the relevant file under /etc/nginx/sites-available/. Throughout this guide, the workflow is identical: make one change, validate, reload.
1sudo nginx -t # validate the full config
2sudo systemctl reload nginx # apply with zero dropped connections
reload re-reads the config gracefully without dropping in-flight connections, unlike restart. Never reload a config you have not just tested.
Stop Leaking the Version
By default Nginx appends its version to the Server response header and to its error pages. That hands a scanner an exact version to match against a CVE list. Turn it off in the http block of /etc/nginx/nginx.conf:
1http {
2 server_tokens off;
3 # ...
4}
After reloading, confirm the header now reads a bare Server: nginx with no version number:
1curl -sI https://example.com | grep -i server
It is a small thing, but it is free, and it removes the easiest piece of reconnaissance an attacker can do against you.
Set Security Headers
Response headers let you instruct the browser to enforce protections on your behalf. Add these inside the server block for your site. The always keyword ensures the header is sent even on error responses:
1add_header X-Content-Type-Options "nosniff" always;
2add_header X-Frame-Options "SAMEORIGIN" always;
3add_header Referrer-Policy "strict-origin-when-cross-origin" always;
4add_header Strict-Transport-Security "max-age=63072000" always;
X-Content-Type-Options: nosniff stops the browser guessing a response's content type, which blocks a class of MIME-confusion attacks. X-Frame-Options: SAMEORIGIN prevents your pages being framed by other origins, defeating clickjacking. Referrer-Policy trims the referrer sent to other sites. Strict-Transport-Security (HSTS) tells the browser to only ever reach you over HTTPS for the next two years — only enable it once TLS is working, because it is sticky and hard to walk back.
A Content-Security-Policy is the strongest header but also the most site-specific. Start in report-only mode so a too-tight policy logs violations instead of breaking your pages:
1add_header Content-Security-Policy-Report-Only "default-src 'self'" always;
Tighten it against real traffic, then switch the header name to Content-Security-Policy once it is clean. One caution: when a location block adds its own add_header, it replaces all inherited headers rather than adding to them, so repeat the full set in any location that needs its own.
Restrict TLS to Modern Protocols
Old TLS versions (1.0 and 1.1) are deprecated and removed from current browsers; serving them only helps a downgrade attack. Pin Nginx to TLS 1.2 and 1.3 with a modern cipher list. These directives go in the http block or the server block that terminates TLS, following Mozilla's intermediate recommendation:
1ssl_protocols TLSv1.2 TLSv1.3;
2ssl_prefer_server_ciphers off;
3ssl_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 is correct for an intermediate config: with TLS 1.3 and modern AEAD ciphers, letting the client choose is preferred. The cipher list above covers every browser released in roughly the last five years while excluding anything weak. If Certbot manages your TLS, it writes its own options-ssl-nginx.conf with similar values — check whether it is already included before duplicating these lines.
After reloading, verify the negotiated protocol from a client:
1curl -sI --tlsv1.2 https://example.com >/dev/null && echo "TLS 1.2 OK"
For a fuller picture, an external scanner such as the SSL Labs server test grades your TLS configuration and flags any protocol or cipher it considers weak. Aim for an A; anything lower usually points at a stray legacy protocol or a missing intermediate certificate in your chain. Two related directives are worth adding once the basics work: ssl_session_cache shared:SSL:10m; reuses negotiated sessions so repeat visitors skip the expensive handshake, and ssl_stapling on; with ssl_stapling_verify on; lets your server staple the certificate's revocation status into the handshake instead of making the browser fetch it separately. Both improve TLS performance without weakening security.
Rate-Limit Abusive Clients
A login form, a search endpoint, or an API route can be expensive enough that a flood from one IP degrades the whole site. Nginx's limit_req module throttles requests per client at the edge, before they reach your application. Define the shared-memory zone once in the http block:
1limit_req_zone $binary_remote_addr zone=req_limit:10m rate=10r/s;
$binary_remote_addr keys the limit on client IP in a compact form; a 10m zone holds state for roughly 160,000 addresses; rate=10r/s sets the sustained ceiling. Then apply it to the routes that need protection, inside the relevant location block:
1location /login {
2 limit_req zone=req_limit burst=20 nodelay;
3 # ... proxy_pass or fastcgi_pass ...
4}
burst=20 absorbs a short spike of up to twenty queued requests so legitimate bursts are not punished, and nodelay serves those burst requests immediately rather than spacing them out — excess beyond the burst is rejected with a 503. Start generous and tighten while watching your access log; a limit set too aggressively will block real users as surely as it blocks a bot.
If you sit behind a reverse proxy or CDN, key the limit on the real client address rather than the proxy's. Set real_ip_header X-Forwarded-For; together with set_real_ip_from for your proxy's range, otherwise every request appears to come from one IP and the rate limit throttles your entire user base at once. You can also return a custom limit_req_status 429; so throttled clients receive the more accurate "Too Many Requests" status instead of a generic 503.
Validate, Then Reload — Every Time
The discipline that ties all of this together is the test-before-reload loop. After each edit:
1sudo nginx -t && sudo systemctl reload nginx
The && is deliberate: the reload only runs if nginx -t reports syntax is ok and test is successful. If the test fails, nothing reloads and the running config — the one currently serving traffic — is untouched. This single habit is what makes hardening Nginx safe to do on a live server: a mistake costs you a failed test, not a downed site.
Hide the version, set the headers, modernise TLS, and cap the request rate on your expensive routes. Each change is independent and reversible, each is validated before it goes live, and together they turn a default Nginx into one that leaks little, speaks only modern TLS, and refuses to be hammered.
References
Posts in this series
- Linux VPS Performance Tuning: sysctl & swap 2026
- Systemd Service Hardening: Sandbox a Unit (2026)
- Fail2ban on Ubuntu VPS: Stop SSH Brute Force 2026
- UFW Firewall Rules for a Public VPS: 2026 Setup
- How to Harden SSH on an Ubuntu VPS (2026 Guide)
- Automated Backups with Rsync and Cron on Linux 2026
- TLS Certificates with Certbot on an Ubuntu VPS 2026
- Harden Nginx on Ubuntu: Headers, TLS & Limits 2026
- Automatic Security Updates on Ubuntu: 2026 Setup