Security headers tell browsers to enable protections that are off by default: force HTTPS, refuse to render your site in hostile frames, block content-type sniffing, restrict which scripts can run. They’re cheap to add and close entire classes of attacks.

A solid baseline

Create a reusable snippet, /etc/nginx/snippets/security-headers.conf:

add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always;

Include it in each HTTPS server block:

server {
    listen 443 ssl;
    server_name example.com;
    include snippets/security-headers.conf;
    # …
}

always adds the header to error responses too (4xx/5xx), not just successful ones.

Also hide the version number globally (in http {}):

server_tokens off;

What each header does

Strict-Transport-Security (HSTS) — browsers will use HTTPS for your domain for max-age seconds, even if someone types http:// or follows an old link. Protects against SSL-stripping attacks. Only send it over HTTPS, and only once HTTPS works everywhere on the domain. Adding includeSubDomains applies it to every subdomain — make sure they all support HTTPS first. preload is a near-permanent commitment. See HSTS explained.

X-Content-Type-Options: nosniff — browsers must respect the declared Content-Type and not guess. Prevents an uploaded “image” from being executed as script.

X-Frame-Options: SAMEORIGIN — only your own site may embed your pages in frames. Prevents clickjacking. (The modern equivalent is CSP frame-ancestors, below.)

Referrer-Policy — controls how much of the URL is sent to other sites when users follow links. strict-origin-when-cross-origin sends only the origin cross-site, and nothing when downgrading to HTTP.

Permissions-Policy — disables browser features you don’t use (camera, microphone, geolocation…), so injected scripts or embedded content can’t request them.

Content-Security-Policy (CSP)

CSP is the most powerful header — it restricts which sources may provide scripts, styles, images, frames and more — and the one most likely to break things. Start in report-only mode:

add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'" always;

Open your site, check the browser console for violations, add the sources you legitimately use (analytics, CDNs, payment providers), and when the console is clean, switch the header name to Content-Security-Policy. Full guide: Content Security Policy.

CMS sites with many plugins (WordPress) often need 'unsafe-inline' for scripts, which weakens CSP considerably; even a partial policy (frame-ancestors, base-uri, object-src 'none', upgrade-insecure-requests) adds protection.

The add_header inheritance trap

Nginx only inherits add_header directives from the enclosing level if the current level defines none. This common setup silently loses the security headers for images:

server {
    include snippets/security-headers.conf;
    location ~* \.(png|jpg)$ {
        add_header Cache-Control "public";    # security headers are now gone here
    }
}

Fix: include the snippet again inside such locations.

Headers you can skip

  • X-XSS-Protection — the browser XSS auditor it controlled has been removed from modern browsers; set it to 0 or omit it. CSP is the real protection.
  • Expect-CT — obsolete.
  • Public-Key-Pins (HPKP) — deprecated and dangerous.

Test

curl -sI https://example.com | grep -iE 'strict-transport|x-content-type|x-frame|referrer|permissions|content-security'

Online scanners such as securityheaders.com and Mozilla’s HTTP Observatory grade your configuration and explain missing headers.

Behind a proxy or CDN

If Cloudflare or another proxy sits in front, headers from your origin are passed through. Some CDNs can also add headers at the edge — make sure the two don’t conflict or duplicate.

Related: Nginx HTTPS redirect and the Linux server hardening checklist.

  • Nginx
  • Security
  • HTTP Headers