Which headers make sense
- Strict-Transport-Security (HSTS): browsers only load the site over HTTPS.
- Content-Security-Policy (CSP): defines where scripts, styles and frames may come from. The most effective protection against XSS, but also the most work.
- X-Content-Type-Options: nosniff: stops browsers from guessing file types.
- Referrer-Policy: limits what is sent when clicking links to other sites.
- Permissions-Policy: switches off camera, microphone, location etc. if you do not need them.
- frame-ancestors (in the CSP) or X-Frame-Options: protection against clickjacking.
X-XSS-Protection is no longer needed. Modern browsers ignore it, and in old ones it could even do harm.
nginx
# /etc/nginx/snippets/security-headers.conf add_header Strict-Transport-Security "max-age=63072000; includeSubDomains" 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=()" always; add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always; # in the server block: include snippets/security-headers.conf; server_tokens off;
nginx has two traps:
- Without
alwaysthe headers are missing on error pages and redirects (e.g. 404, 301). - No inheritance: as soon as a
locationblock has its ownadd_header, the headers from theserverblock no longer apply there. Soincludethe file in thoselocationblocks too.
Apache
# sudo a2enmod headers
<VirtualHost *:443>
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"
Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
Header always unset X-Powered-By
</VirtualHost>
# global, e.g. in security.conf
ServerTokens Prod
ServerSignature Off
On shared hosting without access to the server configuration, the same Header lines usually work in the .htaccess in the document root. Turn off the PHP version in the X-Powered-By header with expose_php = Off in php.ini.
Rolling out a CSP without breaking the site
A CSP that is too strict blocks your own scripts, embedded videos or analytics. So proceed in stages:
- First set it as
Content-Security-Policy-Report-Only. The browser blocks nothing but reports violations in the console. - Go through the console on the most important pages and add the sources you need, e.g.
script-src 'self' https://cdn.example.net. - Move inline scripts into files or give them a nonce.
'unsafe-inline'inscript-srclargely defeats the XSS protection. - Only then switch to
Content-Security-Policy.
Be careful with HSTS
HSTS cannot simply be taken back: browsers remember it for the given time. Start with max-age=300, check that really all subdomains support HTTPS, then raise it to two years. includeSubDomains also covers internal names such as intranet.example.com if they are under the same domain.
Checking
curl -sI https://example.com curl -sI https://example.com/does-not-exist # on 404 too?
The security header check rates all headers, spots typical mistakes such as 'unsafe-inline' and generates the matching lines for your server.
FAQ
Is X-Frame-Options enough or do I need frame-ancestors?
frame-ancestors in the CSP is the modern way and also allows several sources. For very old browsers, adding X-Frame-Options: SAMEORIGIN does no harm.
Why do I see the headers twice?
Usually both the application (e.g. PHP or a CMS) and the web server set them. Duplicate CSP headers are both enforced, so the stricter one wins. Best set them in one place only.
Do APIs need security headers too?
Yes, at least HSTS, nosniff and a tight CSP such as default-src 'none'; frame-ancestors 'none'. They cost nothing and prevent responses from being abused in the browser.
Does a CDN set the headers for me?
Only if you configure it there. Many CDNs simply pass through the origin server’s headers.