Article

How to Add Security Headers in Nginx

Add HTTP security headers in nginx with add_header directives in your server block — using the always flag so they're sent on error responses too — then reload nginx. Here are copy-paste values for HSTS, CSP, X-Content-Type-Options, X-Frame-Options, Referrer-Policy and Permissions-Policy.

By Paul Rudenko, Security ResearcherUpdated Jun 26, 20267 min read

Nginx makes adding HTTP security headers refreshingly simple: a handful of add_header directives in the right place, a config test, and a reload. The details that trip people up are where those directives live and one inheritance quirk that silently drops every header inside certain blocks. This guide gives you copy-paste values and explains the rules so the headers actually reach the browser on every response.

Where do I put add_header directives?

Nginx applies add_header at three levels: the http block (global, in nginx.conf), a server block (one site), or a location block (one path). For most sites the cleanest home is the server block, so the headers cover everything that site serves. If you run many virtual hosts that should all behave identically, the http block is fine too.

Always pass the always parameter. Without it, nginx only emits the header on responses with a 2xx or 3xx status code.

The 4xx and 5xx pages — your 404s, 403s, and 500s — would ship without your security headers, which is exactly when a misconfigured browser or a probing client is poking at the edges of your app. The always flag tells nginx to attach the header regardless of status.

After editing config, validate and reload rather than restart. A reload re-reads the configuration with zero dropped connections; a restart tears the worker processes down. Run nginx -t first — if it reports a syntax error, the reload will refuse and your old config keeps running, so you never serve a broken site.

# Validate the configuration first
sudo nginx -t

# Then reload gracefully (no dropped connections)
sudo systemctl reload nginx
# or, without systemd:
sudo nginx -s reload

The headers

Below is a complete set of add_header directives. Drop them inside your server block. Each line is commented so you know what it does and can tune the values to your needs.

server {
    # ... your listen, server_name, root, etc. ...

    # Force HTTPS for two years and apply to subdomains. Only enable HSTS
    # once you are certain every subdomain serves HTTPS, because it is sticky.
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" always;

    # Stop browsers from MIME-sniffing a response away from its declared type.
    add_header X-Content-Type-Options "nosniff" always;

    # Disallow framing of your pages (clickjacking protection). Use SAMEORIGIN
    # if you legitimately frame your own pages.
    add_header X-Frame-Options "DENY" always;

    # Send only the origin on cross-origin requests; full URL same-origin.
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Switch off powerful browser features you do not use.
    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

    # A conservative Content-Security-Policy. Tune this to your real assets;
    # deploy it in Report-Only first (see the next section).
    add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; object-src 'none'; frame-ancestors 'none'; base-uri 'self'" always;
}

A few notes on these values. The HSTS max-age of 63072000 seconds is two years; add preload only when you intend to submit to the browser preload list and you are confident about HTTPS everywhere.

X-Frame-Options: DENY and CSP frame-ancestors 'none' overlap — modern browsers honour the CSP directive, but keeping X-Frame-Options covers older clients. The Permissions-Policy list above disables geolocation, microphone, and camera; extend the empty allow-lists to match the features your site genuinely uses.

These values track the recommendations of the OWASP Secure Headers Project, which maintains the canonical best-practice value for each header.

Watch your quoting and semicolons

Each directive ends with a semicolon, and the whole header value is wrapped in double quotes. Inside CSP you will use single quotes for keywords like 'self' and 'none' — that is correct and does not conflict with the outer double quotes.

The inheritance trap that drops every header: nginx add_header directives are inherited into a child block only if that block defines no add_header of its own. The moment a location block adds even one header (say, a Cache-Control on a static-files location), it replaces the entire inherited set — and all your security headers silently vanish for that path. Fix it by re-declaring every header inside such locations, or by setting headers only at the server level and avoiding per-location add_header entirely. And remember: without the always flag, every header is dropped on 4xx and 5xx responses too.

How do I roll out CSP safely?

Content-Security-Policy is the most powerful header here and the one most likely to break a working site. A policy that is even slightly too strict will block inline scripts, third-party analytics, fonts, or embedded widgets, and the page will look broken with no obvious cause. Never ship a brand-new CSP straight to Content-Security-Policy.

Instead, deploy it in Report-Only mode first. The browser evaluates the policy and reports what would have been blocked, without actually blocking anything. You watch the reports, widen the policy to cover the assets you legitimately load, and only then promote it to the enforcing header.

# Phase 1: observe, do not enforce. Nothing breaks.
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report" always;

# Phase 2: once reports are clean, switch to the enforcing header.
add_header Content-Security-Policy "default-src 'self'" always;

Building a policy that fits your real asset map — scripts, styles, images, fonts, frames, connections — is its own task. Our Content-Security-Policy guide walks through each directive and how to tighten a policy without breaking the page.

How do I verify it works?

After reloading, confirm the headers are actually on the wire. The fastest check is curl -I, which fetches only the response headers:

curl -I https://example.com

# Look for these lines in the output:
# strict-transport-security: max-age=63072000; includeSubDomains
# x-content-type-options: nosniff
# x-frame-options: DENY
# referrer-policy: strict-origin-when-cross-origin
# permissions-policy: geolocation=(), microphone=(), camera=()
# content-security-policy: default-src 'self'; ...

Crucially, test a path that hits a location block with its own add_header — and test a URL that returns a 404 — to confirm the inheritance trap and the missing always flag are not biting you. If a header is present on the homepage but absent on a static asset or an error page, you have found exactly one of those two issues.

Want to confirm every header reached the browser?

Check your security headers

Where to go next

For the full reference on what each header does, recommended values, and how they fit together, read the complete guide to security headers. Running Apache instead of, or alongside, nginx? The same headers with Apache syntax are in how to add security headers in Apache.

Frequently asked questions

Where do I put add_header in nginx?

Nginx accepts add_header at three levels: the http block in nginx.conf (applies to every site), a server block (one virtual host), or a location block (one path). For most sites the server block is the right home — it covers everything that host serves without you having to repeat the directives per path. Use the http block only when many virtual hosts should behave identically. Avoid scattering add_header across location blocks unless you understand the inheritance rule, because a location with its own add_header discards every inherited header. After editing, run nginx -t to validate the syntax, then reload nginx with systemctl reload nginx or nginx -s reload so the change takes effect with no dropped connections.

Why are my nginx security headers missing on some pages?

Almost always the location-block inheritance trap. Nginx inherits add_header directives from a parent block into a child block only if the child defines no add_header of its own. The instant a location block adds even one header — for example a Cache-Control on a static-assets location — it replaces the entire inherited set, and all the security headers you set at the server level silently disappear for that path. Fix it by re-declaring every security header inside that location, or by keeping all add_header directives at the server level and not using per-location add_header at all. The second common cause is a missing always flag, which drops headers on 4xx and 5xx responses. Test both a static-asset URL and a 404 page to catch these.

Do I need the always flag?

Yes, use always on every security header. Without it, nginx attaches the header only to responses with a 2xx or 3xx status code. That means your 404, 403, and 500 error pages would be served without any security headers — and error responses are precisely the ones a probing or misconfigured client tends to hit. The always parameter tells nginx to emit the header regardless of response status, so HSTS, X-Content-Type-Options, X-Frame-Options and the rest are present on every response. There is no real downside to always for these headers, so make it a habit: add_header HeaderName "value" always;. Verify by running curl -I against a URL you know returns a 404 and confirming the headers still appear.

How do I add a Content-Security-Policy in nginx?

Use add_header Content-Security-Policy with the always flag, but roll it out in two phases. CSP is the header most likely to break a working site, because a policy that is slightly too strict blocks inline scripts, analytics, fonts, or embedded widgets with no obvious error. First deploy add_header Content-Security-Policy-Report-Only with the always flag and a report-uri; the browser evaluates the policy and reports what would have been blocked without actually blocking anything. Widen the policy until the reports are clean, then promote it to the enforcing Content-Security-Policy header. Build the policy directive by directive — default-src, script-src, style-src, img-src and so on — to match the assets you really load. Our Content-Security-Policy guide covers each directive in detail.

Related guides

See your whole external attack surface

One page is a start. The full external scan covers TLS, headers, DNS, exposed files, open services and known-exploited CVEs across your whole domain.

See the full scan →