On this page 10 sections
- Headers that close doors, not a security product
- The core set: HSTS, referrer, framing, content type
- Permissions-Policy and the features you do not use
- CSP in report-only first
- Nonces, hashes and the unsafe shortcuts to avoid
- Cookies, HTTPS and mixed content
- How to read a violation report
- A rollout checklist
- Key takeaways
- Frequently asked questions
I set these on leej.uk at the edge, and I built the Security Headers checker, the CSP Evaluator and the CSP Report Analyser so I could read a policy before it went enforcing. The checklist below is the rollout I trust, not a score to game.
Headers that close doors, not a security product
A header is a precise instruction. X-Content-Type-Options: nosniff stops a browser from guessing a different MIME type. Content-Security-Policy can stop an unexpected script from running, if the policy is correct. Neither will fix a leaked API key, an open form, or a server you have not patched.
Treat them like a privacy manifest: declare what you actually use, and do not claim a tighter world than the page can live in. If the site needs a particular analytics script, the policy must say so, or you must drop the script. A policy that only works because you added 'unsafe-inline' and 'unsafe-eval' everywhere is a slogan.
Check the live response, not the configuration you think you deployed. The HTTP Header Inspector is the habit: look at the headers a client actually received, including after redirects.
The core set: HSTS, referrer, framing, content type
These four are a reasonable baseline for a small HTTPS site.
Strict-Transport-Security tells the browser to use HTTPS for later visits. Only enable it when the host already works on HTTPS and when HTTP already redirects there. Start with a modest max-age (for example a few minutes or a day) and raise it once you are sure you will not need to go back to HTTP. includeSubDomains applies the rule to every subdomain; do not set it until those subdomains are also ready. preload is a further step that asks browsers to hard-code the host into a preload list. That is powerful and slow to undo. Do not add preload or submit the host to a preload list until you have read the current criteria at hstspreload.org and MDN’s HSTS documentation, and you are willing to keep every subdomain on HTTPS.
Strict-Transport-Security: max-age=86400Later, when you are confident:
Strict-Transport-Security: max-age=31536000; includeSubDomainsReferrer-Policy controls what other sites see in the Referer header when someone follows a link. strict-origin-when-cross-origin is a common, conservative default: a full URL on your own site, origin-only on HTTPS cross-origin links, and nothing when downgrading to HTTP. Tighter policies are fine if they still suit your own analytics or payment flows.
Referrer-Policy: strict-origin-when-cross-originFraming. Stop other sites from embedding your pages in an iframe unless you have a reason to allow it. The modern directive is CSP’s frame-ancestors. X-Frame-Options is the older header, still useful for clients that do not implement frame-ancestors. If you send both, they should agree. DENY or frame-ancestors 'none' is the usual choice for a site that is not meant to be framed; SAMEORIGIN or frame-ancestors 'self' if your own origin needs to embed itself.
X-Frame-Options: SAMEORIGIN
Content-Security-Policy: frame-ancestors 'self'You can keep frame-ancestors in the main CSP once you have one, rather than as a separate stub policy.
X-Content-Type-Options: nosniff should be on, and your Content-Type values should be correct, so the browser does not have to guess. This matters for scripts and styles especially.
X-Content-Type-Options: nosniffX-XSS-Protection is legacy and can be ignored or turned off; do not treat it as a modern defence. Other cross-origin isolation headers (Cross-Origin-Opener-Policy, Cross-Origin-Embedder-Policy, Cross-Origin-Resource-Policy) are useful in specific cases and can break third-party embeds and scripts. Add them when you know you need isolation, not as part of a copied starter pack.
Permissions-Policy and the features you do not use
Permissions-Policy (the successor to Feature-Policy) tells the browser which powerful features the page, and the frames it embeds, may use. If you do not need the camera, microphone, geolocation, payment, or USB, say so. That is a close cousin of declaring unused entitlements in an app: a smaller surface, and a clearer story when something asks.
A cautious default for a text-led site might look like this (adjust to the features you actually use, and check the current directive names, which have evolved):
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()If a page later needs geolocation, open it on that origin or that document only, rather than loosening the site-wide header in a hurry. Do not disable a feature you already rely on and then spend an afternoon chasing a “permission denied” in a map widget you forgot about.
CSP in report-only first
Content-Security-Policy is the powerful, sharp header. It can block scripts, styles, images, connections, frames and more. The first time you add it in enforcing mode, you will usually break your own site: an inline script, a typography stylesheet on a CDN, a tool that loads a worker, or a data-URI image.
Start with Content-Security-Policy-Report-Only and a reporting endpoint you can read. The policy should describe the site you intend, not the loosest policy that silences reports. A sketch:
Content-Security-Policy-Report-Only: default-src 'self';
img-src 'self' data:;
font-src 'self';
style-src 'self';
script-src 'self';
connect-src 'self';
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'self';
report-to csp-endpointThen use the site as a person would: every template, every tool, the light and dark theme, the forms, the embeds you actually want. Read the reports. Widen a directive only when the blocked URL is a resource you accept. If the blocked URL is a surprise, find out why it is there before you allow it.
The CSP Evaluator will point out obviously weak directives. It cannot tell you whether a connect-src you added is a vendor you still want. Stay in report-only until a normal week of use produces only reports you understand and have decided to keep or to fix in the page.
When you switch to Content-Security-Policy, keep the report-only header for a while too if you can, so a tightening still has a safety net. Roll this out the way I maintain an iOS app after launch: one controlled change, watched, then the next.
Nonces, hashes and the unsafe shortcuts to avoid
Inline scripts and styles are the usual reason a first policy fails. You have three honest approaches:
- Move the script or style into a file on an origin you already allow under
'self'or a specific host. This is the simplest story. - Use a nonce. Generate a fresh, unpredictable nonce per HTML response, put it on the
<script>or<style>tags you trust, and list'nonce-…'inscript-srcorstyle-src. A static nonce is not a nonce. - Use a hash of the exact inline content, listed as
'sha256-…'. Any edit to the inline block requires a new hash.
What to avoid, unless you are in a temporary, documented exception:
'unsafe-inline'onscript-src, which allows any inline script and much of the XSS protection you wanted from CSP. Onstyle-srcit is still a weakening, though some teams need it for a period while they move inline styles.'unsafe-eval', which allowsevaland similar, and is often a sign of a library you should replace or confine.'strict-dynamic'without understanding it. It can be a good model with nonces, and it can also allow scripts you did not intend if you combine it loosely.- A wildcard
https:inscript-srcordefault-src, which lets any HTTPS host run script.
Third parties will push you towards these shortcuts. Sometimes the honest answer is to drop the third party, or to load it only on one page with a tighter, separate policy, rather than to weaken the whole site. That is the same scope discipline as a careful App Store launch: do not smuggle a dangerous exception in at the last minute because a widget expected it.
Cookies, HTTPS and mixed content
Headers will not save a cookie that is too wide, or a page that still loads a script over HTTP.
- Serve the entire site on HTTPS, with HTTP redirected, before you lean on HSTS.
- Set cookies with
Secure,HttpOnlywhere the cookie does not need to be read by JavaScript, and aSameSitevalue you have thought about (Laxis a common default;NonerequiresSecureand is for deliberate cross-site cases). ScopePathandDomainas tightly as you can. - Eliminate mixed content. A CSP with
upgrade-insecure-requestscan help during a cleanup, but you should still fix the URLs. Once HSTS is on, lingering HTTP subresources are a failure, not a fallback. - Do not put session cookies in a form that a third-party script can read if that script does not need them.
CORS is a separate browser rule for cross-origin requests, covered in the CORS and preflight guide. Caching headers are also separate; a Cache-Control mistake will not be fixed by CSP, and is covered in the HTTP caching guide.
How to read a violation report
A CSP report is a JSON payload describing what was blocked or would have been blocked. Fields you will use, under the older report-uri format or the newer Reporting API, typically include the document URL, the directive that fired, the blocked URL, and whether the policy was enforcing or report-only. Names have shifted between versions, so look at a real payload from your own endpoint rather than memorising a schema from a blog.
When a report arrives, ask:
- Is this a URL I expected, on a page I expected? If yes, allow it in the tightest directive that will work, or change the page to load it differently.
- Is this my own inline script or style? If yes, move it, nonce it, or hash it. Do not jump to
'unsafe-inline'as the first fix. - Is this an extension, a client toolbar, or a password manager injecting into the page? Those reports are noise in the field. Do not widen the policy to please a random extension.
- Is this a typo in the policy, or a stale policy on a cached HTML page? Check the header on the live response.
- Is this a surprise third party? If you did not add it, find the script that did.
The CSP Report Analyser is a way to group a pile of reports so you can see the repeated pairs of document and blocked URL. One unique report from your own walkthrough is often more useful than a thousand field reports you have not sampled.
A rollout checklist
- The site is on HTTPS, HTTP redirects in one hop, and you have fetched the live headers with the HTTP Header Inspector or
curl -sSI. X-Content-Type-Options: nosniffis present, and content types are correct.- A
Referrer-Policyyou have chosen is present. - Framing is restricted with
frame-ancestors, and withX-Frame-Optionsif you are still sending it, in agreement with each other. - HSTS is on with a short
max-agefirst;includeSubDomainsandpreloadare only added after you have read the current requirements and checked every subdomain. Permissions-Policydisables features you do not use, and does not disable a feature a page already needs.- A Content-Security-Policy has run in report-only across every important template, including forms and any interactive tools.
- Reports have been triaged: genuine origins allowed tightly, inline code moved or nonced, surprises investigated, extension noise ignored.
- The policy is now enforcing, still reporting, and does not rely on
'unsafe-inline'or'unsafe-eval'inscript-srcwithout a recorded exception and a plan to remove it. - Cookies are
Secure(andHttpOnly/SameSiteas appropriate), and mixed content is gone. - The Security Headers check and the CSP Evaluator have been run on production, and you agree with any remaining warnings.
These headers also belong on a technical SEO launch checklist for a new site. Revisit the list when you add a third party, a new subdomain, or a payment or map widget. A policy that is not in the same release habit as the HTML will drift, and an enforcing CSP that no longer matches the page is an outage.
Key takeaways
- Security headers close specific browser doors. They do not replace application security, and a copied CSP can break the site.
- Start with a core set: HSTS (carefully), a referrer policy, framing control, and
nosniff. Add a Permissions-Policy for features you do not use. - Roll Content-Security-Policy out in report-only, tighten it from real reports, and prefer nonces, hashes or external files over
'unsafe-inline'and'unsafe-eval'. - Confirm HSTS, cookies and mixed content on the live HTTPS host, and be slow to add
preload. - Inspect live headers and reports whenever the site gains a third party or a new template, in the same release as the change.
If you want a second pair of eyes on a CSP that only works with 'unsafe-inline', tell me about it.
Frequently asked questions
Which security headers does a small website need?
A practical baseline is HTTPS with HSTS (once the host is ready), Referrer-Policy, X-Content-Type-Options: nosniff, a framing policy (frame-ancestors, and X-Frame-Options if you still send it), a Permissions-Policy that turns off unused features, and a Content-Security-Policy rolled out carefully. Add stricter cross-origin isolation headers only when you know you need them.
How do I add a Content-Security-Policy without breaking the site?
Use Content-Security-Policy-Report-Only first, walk every template, and fix or allow the real violations you see. Move or nonce inline scripts rather than starting with 'unsafe-inline'. Switch to enforcing when a normal week of use is quiet, and keep reporting on so later changes still have a net.
What is the difference between CSP report-only and enforcing?
Report-only sends reports, or would-be violations, without blocking the resource. Enforcing blocks the resource and can also report. Report-only is how you learn whether a policy matches the site. Enforcing is the policy that actually protects visitors, and the one that will break a page if it is wrong.
Do I still need X-Frame-Options if I have CSP frame-ancestors?
frame-ancestors is the modern control and is more precise. X-Frame-Options still helps older clients that do not implement frame-ancestors. If you send both, they should mean the same thing. Once you have a CSP, put frame-ancestors in that policy rather than inventing a second, conflicting rule.
How do I add HSTS correctly?
Only after the site works entirely on HTTPS. Start with a short max-age, then raise it. Add includeSubDomains only when every subdomain is ready. Treat preload as a separate, hard-to-reverse step, and read the current preload requirements before you opt in. Check the live header after each change.
Written by Lee Johnson. Published 10 November 2026.