The site opens, the page loads, but the browser console is full of red errors and the button that should work doesn't. Or worse: everything looks fine on desktop, but on mobile part of the page stays white. In nine out of ten cases where I've seen this symptom, someone enabled a security header with a strict value and then forgot that the header also restricts its own domain. A security header is a good tool, but it's like ABS brakes: if you don't know where to apply them, they cause the crash themselves.
First things first, let's see what's currently set on the server. A simple command is enough:
curl -sI https://example.com | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy'
If the output is empty, you have no security headers. If it's full, you first need to understand exactly what each one does, because the order of enabling them matters more than the enabling itself.
What attack does each security header block
Headers can be divided into two categories: those that stop the browser, and those that stop the attacker. The first category takes effect quickly and carries low risk. The second has a deeper effect, but if configured wrong, it takes the site offline for users.
| Header | Attack it blocks | Risk of misconfiguration |
|---|---|---|
X-Content-Type-Options: nosniff | MIME sniffing and execution of uploaded files as scripts | Almost zero |
X-Frame-Options: DENY | Clickjacking | Low, unless you embed the site in an iframe |
Referrer-Policy | Leaking internal URLs to external sites | Low |
Strict-Transport-Security | SSL stripping and downgrade to HTTP | High if you preload |
Content-Security-Policy | XSS, script injection, data theft | High if you start strict |
It's the last two headers that cause all the trouble. The rest you can safely add today.
X-Content-Type-Options and Referrer-Policy: start here
Put these two lines in your web server config and move on to the next task:
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;
The word always matters in Nginx. If you omit it, these headers aren't applied to error responses like 404 and 500, and those error pages become the entry point. I've seen this a lot in security reviews.
HSTS: why preload is a trap
The Strict-Transport-Security header tells the browser to open this domain only over HTTPS for a specified period. Simple:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
The number 31536000 means one year, in seconds. Now the trap: only set includeSubDomains when you're sure all your subdomains have valid HTTPS. If you have an old subdomain that's HTTP-only, from the moment the browser sees this header, that subdomain becomes unreachable for all users who have previously visited the site. Not by clearing the cache, not by changing DNS. You have to wait for max-age to expire.
And then preload. Preload means registering your domain in the hardcoded list compiled into the browser itself. From that moment, even the first visit is forced to HTTPS and there's no way back. I only recommend preload for a domain that has run with HSTS without issues for at least six months, has all its subdomains on HTTPS, and has no plans to migrate to another domain. For everyone else, a one-year max-age is enough.
If you don't yet have a valid certificate on the server, fix that first; the guide on SSL errors and how to diagnose them is a good starting point. HSTS on broken HTTPS only makes the failure faster.
CSP: the most effective security header and the riskiest
Content-Security-Policy is the only header that truly blocks XSS. The other headers are a defense layer; CSP is the main wall. But it's also CSP that turns sites white.
The common mistake is copying from some online guide and setting this:
add_header Content-Security-Policy "default-src 'self'; script-src 'self'" always;
And then your site is full of inline scripts that none of them run. The menu button doesn't work, the contact form doesn't submit, and since the error is in the console and not in the server log, you spend hours hunting for a bug in the code.
Start with Report-Only, not enforce
The correct order is this. First enable it in reporting mode:
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' https://cdn.example.com; report-uri /csp-report" always;
The browser blocks nothing, but sends everything that's violated to that address. Collect logs for a week. Then you'll find out which domains you actually need, not the ones you thought you did. After a week, set the same policy without -Report-Only.
A practical note: if your site is WordPress, a strict CSP almost always breaks plugins, because many of them inject inline scripts. Before touching CSP, first remove unnecessary plugins; the guide on WordPress security from wp-config to plugins walks through this step by step. If your site's REST API is open, that's also a data leak path, and you should separately do closing the user data leak from the REST API.
nonce and hash: the right way for inline scripts
Instead of 'unsafe-inline', which nullifies the whole CSP, use a nonce. The web server generates a random value for each response and puts it in the header:
script-src 'nonce-rAnd0m123' 'strict-dynamic';
And you put the same value in the script tag: <script nonce="rAnd0m123">. It's work, but it's the only way to have both a real CSP and working inline scripts. If the site is static, use a hash, which is simpler.
The order that actually works
- Add
X-Content-Type-Options,Referrer-Policy, andX-Frame-Optionstoday. Zero risk. - Fix HTTPS on all subdomains and set up a 301 redirect from HTTP.
- Start HSTS with
max-age=300. Wait a week. Then raise it to one year. - Set CSP with
-Report-Onlyand collect logs for a week. - Enforce the final policy, with nonce for inline scripts.
- Consider preload only if six months have passed without issues.
Follow this order, because each step builds the assumptions for the next. If you set CSP before cleaning up inline scripts, you've just wasted a week.
There's also an infrastructure point that's often overlooked: security headers can also be set at the network edge, not just on the web server. If traffic passes through a DDoS protection layer, some of these headers can be applied there too, which means that even if someone hits the server IP directly, the headers aren't lost. For sites that are targets of volumetric attacks, this layer is worth it.
And finally: record headers in an audit log. Every time someone changes a policy, you should know when and why. If you don't have an audit log yet, read the guide to keeping an intact audit log; without it, debugging a broken CSP in the future is nearly impossible.
Frequently asked questions
Why did part of the site go white after adding CSP?
Because CSP in enforce mode blocks any resource not listed in the policy, and the browser removes it instead of displaying it. The first thing you should do is open the browser console and look for the Refused to load or Refused to execute inline script error. That error tells you exactly which domain or which inline script was blocked. Until you've seen it, any change to the policy is guesswork.
Can HSTS take a site offline forever?
Normally no, because after max-age expires the browser allows HTTP again. But if you've registered the domain in the preload list, yes; that list is inside the browser itself and won't be removed until a new browser version is released and the user updates. That's why I only recommend preload for a domain you're sure will stay on the same domain for years and has all its subdomains on HTTPS.
Does a security header replace a firewall or WAF?
No. A security header runs in the user's browser, so it only affects users who come to the site through a browser. An attack that hits the API directly or is server-to-server sees none of these headers. Headers are a client-side defense layer and should be alongside network protection and server-side authentication, not instead of them.
How do I know my headers are set correctly?
With curl -sI on the domain, or with online header-checking tools. But be sure to test several different paths: the homepage, an internal page, a 404 page, and an API response. Often headers are only applied to 200 responses and error pages are left unprotected. If your site is behind multiple layers, test from outside the network, not from inside the server.
Comments 0
No comments yet — be the first!