Guide

Hardening, Headers and Logging

Part 8 of 8By Amith Kumar7 min read
Part 8 of 8Nginx in Production: A Hands-On Guide
  1. 1Install Nginx and Its Config Model
  2. 2Serving Static Sites and Server Blocks
  3. 3Reverse Proxy to an App
  4. 4HTTP/2, HTTP/3 and Compression
  5. 5Caching, Static and Proxy
  6. 6Rate Limiting and Abuse Control
  7. 7Load Balancing App Instances
  8. 8Hardening, Headers and Logging
Verified 22 Jul 2026 · Ubuntu 24.04 LTS · nginx 1.24.0.

Nginx hardening: the difference between working and safe

This is the last piece of the destination from chapter one: the hardened response. A default nginx serves pages, but it also announces its version, omits the headers that protect your users, and logs in a way you will regret during an incident. nginx hardening closes those gaps: hide what attackers can use, add the headers browsers enforce on your behalf, and log so you can answer questions later. This chapter applies each on a live server and verifies it.

Key takeaways
  • server_tokens off stops Nginx from advertising its exact version, removing a free hint for anyone matching servers to known exploits.
  • A handful of security headers, HSTS, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, and a Content-Security-Policy, let the browser enforce protections you cannot enforce alone.
  • The always flag on add_header is essential, or the headers vanish on error responses, exactly when they matter.
  • Access and error logs plus logrotate are what let you investigate an incident instead of guessing; a fail2ban jail turns those logs into automatic blocking.

Your Nginx works. It serves your app, over HTTP/2, compressed, cached, load-balanced. But working is not the same as safe, and the gap between them is a short list of settings that almost no default turns on. None of these change what your users see. All of them change what an attacker sees, what a browser will enforce, and what you can find out when something goes wrong. This is the chapter that makes the server one you would run in production without wincing.

Stop advertising your version

By default Nginx puts its exact version in the Server response header and on its error pages. That is a free gift to anyone scanning for servers running a version with a known vulnerability. Turn it off:

server_tokens off;

Set it once in the http context and it applies everywhere. Verify it:

curl -I https://your-domain
With server_tokens off, the Server header now reads just nginx, no version to fingerprint

The Server header now reads just nginx, with no version. It is a small thing, but security is largely the accumulation of small things that each remove an attacker's shortcut. This removes one.

Checkpoint

The Server header should read nginx with no version, exactly like the destination response in chapter one. If it still shows nginx/1.24.0 (Ubuntu), server_tokens off is not in the http context or the reload did not happen.

Headers the browser enforces for you

Some protections can only happen in the browser, and you switch them on by sending headers. A few are worth setting on every site:

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;
add_header Content-Security-Policy "default-src 'self'" always;

Strict-Transport-Security tells the browser to only ever use HTTPS for your domain, closing the window where a first plain request could be intercepted. X-Frame-Options: SAMEORIGIN stops other sites from embedding yours in a frame to trick your users, the clickjacking defence. X-Content-Type-Options: nosniff stops the browser from guessing content types and running something it should not.

Referrer-Policy controls how much of your URL leaks to other sites when a user clicks away. And Content-Security-Policy is the strong one: it tells the browser exactly which sources of scripts, styles, and images to trust, which is the single most effective defence against cross-site scripting.

Confirm they arrive:

curl -I https://your-domain
All five security headers present on the response: HSTS, X-Frame-Options, nosniff, Referrer-Policy and CSP

All five appear in the response. Note the always flag on each line, and do not drop it. Without always, nginx omits these headers on error responses like 404s and 500s, which are exactly the responses an attacker probes. always sends them on every response, which is what you want.

Checkpoint

Your response should now carry all five headers: HSTS, X-Frame-Options, nosniff, Referrer-Policy and a Content-Security-Policy. That is the full hardened response you were shown at the very start of this series, now served by your own server. Request a missing URL too; with always, the headers hold on the 404.

A word on Content-Security-Policy: default-src 'self' is a strict starting point that only allows resources from your own domain. Real apps usually load a few third-party scripts and will need those sources added, so build the policy up from strict rather than starting permissive. It is the one header worth spending real time on, because a good CSP defangs most XSS.

Logs you can actually use

When something goes wrong at 2 am, your logs are the difference between an answer and a guess. Nginx writes an access log of every request and an error log of every problem, and the defaults are decent. The two things to add are enough detail and rotation.

A custom log format that records the real client IP, the response time, and the upstream that served it makes incidents tractable:

log_format detailed '$remote_addr - $status "$request" '
                    '${request_time}s upstream=$upstream_addr';
access_log /var/log/nginx/access.log detailed;

Now every line tells you who, what, how it ended, how long it took, and which backend handled it. That is what you grep during a slowdown or a probe.

Logs grow, so rotate them. Ubuntu ships a logrotate config for Nginx already, which compresses and ages out old logs daily, so you do not fill the disk. Confirm it is active and understand it rather than reinventing it; the machinery is already there.

Going further: turn logs into automatic blocking

Logs are more than a record. fail2ban watches them and bans IPs that misbehave, the same tool you met hardening SSH, pointed at Nginx. A jail that watches for repeated 4xx probing or auth failures turns a noisy attacker into a blocked one without you lifting a finger:

[nginx-badbots]
enabled  = true
filter   = nginx-badbots
logpath  = /var/log/nginx/access.log
maxretry = 5

Now a client that trips the filter enough times is dropped at the firewall for a while, automatically. Your logs stopped being a passive record and became an active defence.

Frequently asked questions

Why do my security headers disappear on error pages?

Because add_header without the always flag only applies to successful responses. Add always to each header so they are sent on 404s and 500s too, which are exactly the responses an attacker tends to probe.

What does server_tokens off actually hide?

It stops Nginx from printing its exact version in the Server response header and on error pages. That removes a free hint for anyone scanning for a version with a known vulnerability, so the Server header just reads nginx.

Which security header should I spend the most time on?

Content-Security-Policy does the most work, because a good policy tells the browser exactly which script, style, and image sources to trust and defangs most cross-site scripting. Start from default-src 'self' and add only the sources your app genuinely needs.

How does fail2ban block abuse using Nginx logs?

fail2ban watches the access log with a filter, and when an IP trips it enough times, such as repeated 4xx probing, it bans that IP at the firewall for a while. Your logs stop being a passive record and start blocking attackers automatically.

Recap: the front door is built

You can run nginx the way you would in production: version hidden, the browser enforcing HTTPS, framing rules, and a content policy on your behalf, with the always flag so those protections hold on every response. You can log enough to investigate an incident and rotate so you never run out of disk, and you can point fail2ban at those logs to block abuse automatically.

That closes the series. You started with a bare slice and nothing installed, ran apt install nginx, saw the default page in a browser, then served your own site, put your app behind the proxy, and made it fast with HTTP/2 and compression, quiet with caching, safe under load with rate limiting, resilient with load balancing, and finally hardened. The response you were shown in chapter one, HTTP/2, version hidden, security headers and all, is now the one your own server returns. That is a production-shaped front door for anything you run.

Run your hardened site on a slice

Every block in this series lives in one nginx config on one box with a public IP. Launch a slice and stand up the fast, hardened front door you just built, end to end.


Put it live on your own slice

Run your site behind Nginx on a slice

Nginx needs a server with a public IP to be your front door. Spin up a slice, put your app behind the hardened reverse proxy from this guide, and serve it fast and safe.

Get early access

This guide is provided for educational purposes and offered as is, without warranty of any kind. The commands change the configuration of a server you control, and some can lock you out if run out of order. Test on a non-production machine, keep a second session open, and take a snapshot or backup before you begin. You are responsible for changes made to your own systems; ServerCake accepts no liability for any loss, downtime, lockout, or damage arising from following this guide or running the accompanying script.

Was this guide helpful?
Share

Spotted something out of date or have a question?

Built by the ServerCake team

Cloud that speaks India.

ServerCake is India's KYC-verified cloud: VMs and managed databases priced in ₹, billed with GST, on India-resident infrastructure. Reserve your spot for early access.

Reserve your spot