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.
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.
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.
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;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.
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.
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.
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.
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.
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:
# /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:
server {
listen 443 ssl;
server_name example.com;
include snippets/security-headers.conf;
# ...
}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
# 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.comOr 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.