Security headers are HTTP response headers that tell browsers to enable built-in protections: force HTTPS, block clickjacking, stop MIME sniffing and limit where scripts can load from. They cost nothing in performance and are set once in the web server. This guide covers the headers worth sending and how to add them in Nginx and Apache.
Before you start
- Root access to an Nginx or Apache server, with HTTPS already working.
- On cPanel hosting with LiteSpeed, use
.htaccessinstead: see LiteSpeed .htaccess security headers.
The security headers that matter
| Header | Protects against | Recommended value |
|---|---|---|
| Strict-Transport-Security | Downgrade to HTTP, cookie theft | max-age=31536000; includeSubDomains |
| Content-Security-Policy | XSS, injected scripts, clickjacking | Site-specific (see below) |
| X-Content-Type-Options | MIME sniffing | nosniff |
| X-Frame-Options | Clickjacking (legacy browsers) | SAMEORIGIN |
| Referrer-Policy | Leaking full URLs to other sites | strict-origin-when-cross-origin |
| Permissions-Policy | Unwanted camera, mic, location use | camera=(), microphone=(), geolocation=() |
Skip X-XSS-Protection: modern browsers removed the filter it controlled. Set it to 0 or leave it out.
Add security headers in Nginx
Put them in the HTTPS server block, or in a snippet you include:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" 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=()" always;Warning: Nginx add_header directives inherit only if the inner block has none. A location with its own add_header (for example for caching) drops all server-level headers. Include the snippet in that location too.
The always parameter sends the headers on error responses as well.
Add security headers in Apache
Enable mod_headers (sudo a2enmod headers), then add to the <VirtualHost *:443> block:
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"Roll out Content-Security-Policy safely
CSP is the most powerful header and the easiest to break a site with, because it blocks any script, style, font or frame not on its allow list.
- Start in report-only mode so nothing is blocked:
add_header Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https:; script-src 'self'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'" always; - Browse the whole site with the browser console open. Each violation names the blocked source.
- Add legitimate sources (analytics, fonts, payment widgets) to the right directive.
- When the console is clean, rename the header to
Content-Security-Policy.
frame-ancestors 'self' is the modern replacement for X-Frame-Options; sending both is fine.
Check the result
curl -sI https://example.com/ | grep -iE 'strict|content-security|x-frame|x-content|referrer|permissions'Test a 404 URL too, to confirm the headers appear on errors.
Troubleshooting
- Headers missing on some pages: an Nginx location with its own
add_header, or PHP/the app sending its own headers. Check per URL with curl. - Headers sent twice: both the web server and the app (or a plugin) set them. Keep one source.
- Site broken after CSP: switch back to Report-Only and add the missing sources.
FAQ: security headers
Should I add HSTS preload?
Only when every subdomain serves HTTPS permanently. Preload is hard to undo. See the HSTS header guide.
Do security headers improve SEO?
Not directly, but HTTPS enforcement and a safe site protect rankings and visitors.
Where do headers go behind a reverse proxy?
Set them on the server that talks to browsers, usually the proxy or TLS terminator.
