Security headers are instructions your server sends along with every page: “only use HTTPS”, “do not guess file types”, “do not let other sites put me in a frame”. The browser enforces them. They do not fix vulnerable code, but they shrink what an attacker can do with one, and they cost a few lines of configuration. Yet a quick scan of most small business sites shows an empty list.
The headers that matter
- Strict-Transport-Security (HSTS): tells the browser to use only HTTPS for your domain for a given time. Stops downgrade and cookie-sniffing on hostile networks. Start with
max-age=31536000; includeSubDomains. - Content-Security-Policy (CSP): lists where scripts, styles, images and frames may load from. It is the strongest defence against cross-site scripting, and the only one that takes real work.
- X-Content-Type-Options: nosniff: the browser must trust the declared content type instead of guessing, which closes a class of upload-based attacks.
- X-Frame-Options or CSP
frame-ancestors: prevents other sites from embedding yours in a frame (clickjacking). - Referrer-Policy: controls how much of the URL leaks to other sites.
strict-origin-when-cross-originis a sane default. - Permissions-Policy: switches off browser features you do not use, such as camera, microphone or geolocation.
- Cross-Origin-Opener-Policy: isolates your window from pop-ups opened by other origins.
A starter set for nginx
This is close to what runs on this site. Put it in the server block (or an included file):
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
add_header Cross-Origin-Opener-Policy "same-origin" always;
add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; frame-ancestors 'self'" always;
server_tokens off;
Then check and reload: nginx -t && systemctl reload nginx.
Two mistakes that break sites
1. add_header disappears in nested blocks
In nginx, as soon as a location block contains its own add_header, it stops inheriting the ones from the server block. A cache header in one location can silently remove all your security headers there. Put the headers in one included file and repeat the include wherever a location sets its own headers. Always test a static file and a PHP or proxied URL, not only the home page.
2. A CSP that is too strict, switched on in one go
The CSP above is deliberately small. A real site often loads fonts, analytics or inline scripts, and a strict policy blocks them. Roll it out in two steps:
- Send
Content-Security-Policy-Report-Onlyfirst. Nothing breaks, but the browser console lists what would have been blocked. - Fix or allow-list what you find, then switch to the enforcing header.
Avoid 'unsafe-inline' in script-src: it removes most of the protection. If you have a single inline script, allow it by its hash ('sha256-…') instead.
Remove what leaks
server_tokens off; hides the nginx version. Also check for X-Powered-By from PHP or the hosting panel: it tells attackers which software to target, and it is easy to miss because it is added in a different layer than the rest of your headers.
Test it
From a terminal:
curl -sI https://example.com/ | grep -iE 'strict|content-security|x-frame|x-content|referrer|permissions|cross-origin|x-powered|server'
Or paste the address into the free security header checker: it follows redirects, grades the result from A to F, and tells you which header is missing or too weak. No sign-up, nothing is stored.
A sensible order
- HSTS, nosniff, Referrer-Policy, Permissions-Policy: safe on almost any site, do them now.
- Frame protection:
SAMEORIGINunless you deliberately embed the site elsewhere. - CSP in report-only mode, then enforcing.
- Re-test after every change to your proxy, panel or CDN, because each layer can add or drop headers.
Headers are not a replacement for patching. If you are on nginx, the latest posts cover current advisories; these headers are the cheap layer that makes the next one hurt less.