HTTP security headers are instructions your server attaches to every response, telling the browser how cautious to be with your site. They cost nothing at runtime, most are a single line of configuration, and they are absent from a great many sites simply because nobody has gone through the list.
There are more than six in existence. These are the six worth the configuration.
Strict-Transport-Security (HSTS)
Strict-Transport-Security: max-age=31536000; includeSubDomains
Tells the browser to use HTTPS for your domain for the next year, without trying HTTP first. This closes the gap where someone typing your domain gets one unencrypted request before the redirect - a request that can be intercepted on a hostile network.
Two cautions. includeSubDomains covers every subdomain, so confirm they all serve HTTPS before publishing it. And the policy is hard to retract: the browser honours it for the full max-age, so a site that later cannot serve HTTPS becomes unreachable rather than insecure. Start with a shorter max-age if you want an easier exit.
Content-Security-Policy (CSP)
Content-Security-Policy: default-src 'self'; img-src 'self' data:;
style-src 'self' 'unsafe-inline'; script-src 'self'; frame-ancestors 'self'
The most powerful of the six and the only one that can break your site. CSP declares where resources may load from, so an injected script from an origin you never approved simply does not execute.
It is also the one that requires testing, because real sites load things from more places than anyone remembers. Deploy it in report-only mode first:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Nothing is blocked; violations are reported. Run it for a week, look at what would have broken, adjust, then switch to enforcing.
Note that 'unsafe-inline' for scripts largely defeats the purpose - it permits exactly the injected inline script CSP exists to stop. If your site relies on inline scripts, moving them to files is the work that makes CSP meaningful. Being honest about it: a policy with script-src 'unsafe-inline' will pass most automated checks while providing very little of the protection the header is for.
X-Frame-Options
X-Frame-Options: SAMEORIGIN
Stops your pages being embedded in a frame on someone else's site, which is the mechanism behind clickjacking - a transparent frame over a page the victim believes they are interacting with.
Modern equivalent is CSP's frame-ancestors, which is more expressive. Send both: frame-ancestors for browsers that honour it, X-Frame-Options for anything older.
X-Content-Type-Options
X-Content-Type-Options: nosniff
One value, no options, no downside. Stops the browser second-guessing your declared content type. Without it, a file you serve as text can be interpreted as JavaScript if the browser thinks it looks like JavaScript - which matters most for user-uploaded content.
If you add exactly one header today, this is the one with the best ratio of protection to risk.
Referrer-Policy
Referrer-Policy: strict-origin-when-cross-origin
Controls how much URL information is sent when a visitor follows a link off your site. The default behaviour leaks full URLs to third parties, which matters if your paths contain anything sensitive - a reset token, an internal identifier, a customer reference.
The value above sends the full URL within your own site, and only the origin when leaving it. Sensible for nearly everyone.
Permissions-Policy
Permissions-Policy: geolocation=(), microphone=(), camera=()
Declares which browser features your site may use. Empty parentheses mean "not at all, including in embedded frames". If your site does not need the camera, saying so removes a capability that any injected or embedded content could otherwise attempt to reach.
The six at a glance
| Header | Stops | Risk of adding |
|---|---|---|
| Strict-Transport-Security | Downgrade to plain HTTP | Medium. Hard to retract |
| Content-Security-Policy | Injected script execution | High. Test first |
| X-Frame-Options | Clickjacking via framing | Low |
| X-Content-Type-Options | MIME type guessing | None |
| Referrer-Policy | URL leakage to third parties | Low |
| Permissions-Policy | Unwanted use of camera, mic, location | Low |
Adding them, in order
On Apache or LiteSpeed - which includes most shared hosting - all six go in .htaccess:
<IfModule mod_headers.c>
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 "geolocation=(), microphone=(), camera=()"
</IfModule>
A sensible sequence:
- Add the four safe ones first - nosniff, X-Frame-Options, Referrer-Policy, Permissions-Policy. These essentially never break anything.
- Add HSTS once you have confirmed every hostname serves HTTPS properly.
- Work on CSP separately, in report-only mode, over a week or two. It deserves its own change rather than being bundled with the others.
Then verify from outside, because a header set in config is not the same as a header actually sent - a CDN or proxy in front of your origin can strip or override them:
curl -sI https://yourdomain.com | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy'
'unsafe-inline' scripts passes the presence test while doing little. Present is not the same as effective.
Headers you can safely ignore
Several headers still appear in older hardening guides and in some scanner output. Adding them is not harmful, but it is not useful either, and knowing which are obsolete saves arguing about them.
X-XSS-Protectioncontrolled a browser XSS filter that has been removed from modern browsers. In some older versions the filter introduced its own vulnerabilities, which is why it was dropped. Content-Security-Policy replaced it.Expect-CTis obsolete. Certificate Transparency enforcement is now built into browsers directly, so the header does nothing.Feature-Policyis the previous name for Permissions-Policy. Use the current one; there is no reason to send both unless you need to support quite old browsers.Public-Key-Pinswas removed from browsers, and was dangerous while it lasted: a pinning mistake could make a site unreachable for the duration of the policy.
If a tool flags any of these as missing, that is a signal about the tool rather than your site.
When a CDN is in the way
If something sits between your origin and your visitors - a CDN, a reverse proxy, a platform's edge - then the headers you configure and the headers your visitors receive are two different things.
Three failure modes, all of which produce a report that disagrees with your config file:
- The edge strips them. Some configurations pass through only a known set of headers.
- The edge adds its own. You may end up with two
X-Frame-Optionsheaders with different values, and browser behaviour when headers conflict is not something to rely on. - The edge serves cached responses from before your change, so the new headers appear only after the cache expires.
The resolution is the same in each case: verify from outside, against the public hostname, not against the origin. A header that exists in .htaccess but never reaches a browser is not protecting anyone, and this is the single most common reason a headers check fails on a site where someone has genuinely configured them.
What they are and are not
Headers are a browser-side layer. They reduce the impact of certain client-side attacks; they do nothing about an insecure API, a weak password policy or an exposed credential.
What makes them worth doing is the ratio. Five of the six are one line each, they break nothing, and they are visible to anyone assessing you from outside - which is why they appear on every external report, including in your external security posture. The sixth, CSP, is a genuine piece of work, and the only one on this list where the header being present tells you considerably less than you would hope.