How to Set Security Headers in Nginx

A complete guide to configuring all seven essential HTTP security headers in Nginx using add_header directives, including the recommended values and testing steps.


Nginx makes it straightforward to add HTTP security headers using the add_header directive. This guide covers all seven essential headers — HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, and X-XSS-Protection — with recommended values and a ready-to-use configuration block.

Prerequisites

  • Nginx installed and serving your site over HTTPS.
  • Access to your Nginx server configuration (typically in /etc/nginx/sites-available/ or /etc/nginx/conf.d/).
  • A valid SSL certificate (required before enabling HSTS).

Complete Configuration

Add the following add_header directives inside your server block (the one listening on port 443). The always flag ensures the headers are sent on all responses including error pages.

nginx
server { listen 443 ssl; server_name example.com www.example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # ── Security Headers ────────────────────────────────────────────────── # Force HTTPS for 1 year; include subdomains; opt into browser preload list add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; # Restrict resource loading to same origin; block inline scripts and styles # Adjust script-src and style-src to match your CDN and third-party needs add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always; # Prevent clickjacking — block all iframe embedding add_header X-Frame-Options "DENY" always; # Prevent MIME-type sniffing add_header X-Content-Type-Options "nosniff" always; # Send origin only on same-site requests; send no referrer on cross-site downgrades add_header Referrer-Policy "strict-origin-when-cross-origin" always; # Block sensitive browser features from the page and all embedded iframes add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), fullscreen=(self), payment=()" always; # Explicitly disable the defunct IE/Chrome XSS Auditor (modern browsers ignore this) add_header X-XSS-Protection "0" always; # ── End Security Headers ────────────────────────────────────────────── root /var/www/example.com; index index.html; location / { try_files $uri $uri/ =404; } } # Redirect all HTTP traffic to HTTPS server { listen 80; server_name example.com www.example.com; return 301 https://$host$request_uri; }

Header-by-Header Explanation

Strict-Transport-Security (HSTS)

Tells browsers to only connect to your site over HTTPS for the duration of max-age (1 year in this example). includeSubDomains extends the policy to all subdomains. Only add preload once all subdomains serve valid HTTPS — it commits your domain to the browser preload lists, which is hard to reverse.

nginx
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

Content-Security-Policy (CSP)

Restricts which resources (scripts, styles, images, fonts) the browser is allowed to load. The example policy uses a strict default-src 'self' baseline. Adjust script-src to include any CDN or analytics domains your site requires.

nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always;
Test CSP in Report-Only Mode FirstUse Content-Security-Policy-Report-Only instead of Content-Security-Policy while testing. Violations are reported but not blocked, so you can tune the policy without breaking your site.

X-Frame-Options

Prevents your pages from being embedded in iframes on other sites, blocking clickjacking attacks. DENY blocks all framing. Use SAMEORIGIN if your own pages embed each other in iframes.

nginx
add_header X-Frame-Options "DENY" always;

X-Content-Type-Options

Prevents browsers from MIME-sniffing a response away from the declared Content-Type. This stops attackers from uploading a file with a misleading extension that the browser then executes as a script. nosniff is the only valid value.

nginx
add_header X-Content-Type-Options "nosniff" always;

Referrer-Policy

Controls how much of the page URL is sent in the Referer header to other sites. strict-origin-when-cross-origin is the recommended balance: sends full URL on same-origin navigations, origin only on cross-origin HTTPS, and nothing on cross-origin HTTP downgrades.

nginx
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

Permissions-Policy

Restricts which browser features (camera, microphone, geolocation, etc.) can be used by your page and any embedded iframes. The example disables all sensitive features by default and allows fullscreen for your own origin only. Adjust to match your application's needs.

nginx
add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), fullscreen=(self), payment=()" always;

X-XSS-Protection

Set to 0 to explicitly disable the now-defunct XSS Auditor built into older browsers. Chrome removed the auditor in 2019, Safari in 2022. Sending 0 prevents any remaining IE11 instances from running the legacy (and potentially exploitable) filter. Modern XSS protection comes from your CSP.

nginx
add_header X-XSS-Protection "0" always;

Using a Shared Headers File

If you host multiple sites on the same Nginx instance, avoid duplicating headers in every server block. Extract them into a shared file:

nginx
# /etc/nginx/snippets/security-headers.conf add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" always; add_header X-Frame-Options "DENY" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), fullscreen=(self), payment=()" always; add_header X-XSS-Protection "0" always;

Then include it in each server block:

nginx
server { listen 443 ssl; server_name example.com; include snippets/security-headers.conf; # ... }
add_header InheritanceIn Nginx, add_header directives defined in a parent context (e.g., http or server) are not inherited by child contexts that define their own add_header directives. If you have add_header in a location block, you must repeat all security headers there too, or use the include approach above.

Testing and Applying Changes

bash
# Test the configuration for syntax errors sudo nginx -t # Reload Nginx to apply changes (zero downtime) sudo systemctl reload nginx # Verify headers are being sent curl -I https://example.com

Or use the ShowDNS Security Headers Scanner to check all headers and get a full grade report.

Frequently Asked Questions

Why use always in add_header?

Without always, Nginx only adds the header on 2xx and 3xx responses. With always, it is added on all responses including 4xx and 5xx error pages. Security headers should apply universally, so always is the correct choice.

Can I set security headers in the http block?

Yes, but be aware of the inheritance limitation: any child server or location block that defines its own add_header will not inherit the parent's headers. The shared snippet include approach avoids this problem.

My CSP is breaking my site. What should I do?

Switch to Content-Security-Policy-Report-Only mode, check the browser console for violation reports, and adjust your policy to permit the resources your site legitimately loads. Only switch back to the enforcing header once violations are resolved.

Related Articles