Zero Security Headers to a Hardened Static Site: The Cloudflare Recipe I Run on NitroIDE
GitHub Pages is a wonderful host with one sharp edge: it cannot set HTTP response headers. Not one. Your site ships with zero security headers unless you add a layer in front of it that can.
I build NitroIDE — a browser-based IDE where your code never leaves your machine. When I audited my own site for the v25 release, the header situation was blunt: none. No HSTS, no nosniff, no referrer policy, nothing. This post is the full recipe I run today, header by header, with the exact values, what each one stops, the price I paid for the strict ones, and the two headers I deliberately left out.
Where the headers live
Cloudflare sits in front of nitroide.com, and Cloudflare's Transform Rules → Modify Response Header is where every header below is set. One rule, applied to all responses, no code changes, no redeploy to change a value.
Two alternatives I ruled out: a _headers file only works if your host reads it (Cloudflare Pages does; GitHub Pages does not — my origin ignores it), and <meta http-equiv> tags in HTML only cover a couple of headers and don't apply to non-HTML responses. If your static site is behind Cloudflare, the Transform Rule is the one place that covers everything.
The recipe
HSTS — Strict-Transport-Security: max-age=31536000; includeSubDomains
Forces the browser to use HTTPS for the next year, for this domain and every subdomain. It kills SSL-stripping and downgrade attacks — the kind where an attacker on public Wi-Fi serves your site over plain HTTP and the user never notices. One deliberate omission: no preload. Preload is a one-way door — once you're on the browser preload list, every subdomain must serve valid HTTPS forever, and removal takes months. One year + includeSubDomains is the strong posture without the irreversible commitment.
MIME sniffing — X-Content-Type-Options: nosniff
Stops browsers from second-guessing your Content-Type. Without it, a browser can decide a file you served as text/plain is actually a script and execute it — the classic vector for turning an upload or a user-content endpoint into code execution. One word, no downside. There is no reason to skip this one.
Referrer — Referrer-Policy: strict-origin-when-cross-origin
Same-origin requests send the full URL; cross-origin requests send only the origin. Your analytics and your own logs keep the detail, but when a user clicks out to another site, that site learns they came from nitroide.com — not which page, not which ?code= share link, not which query string. Share links carry user code; leaking their full path in a referrer would be a quiet privacy hole.
Permissions — Permissions-Policy: camera=(), microphone=(), geolocation=()
Turns off powerful browser features the site never uses. NitroIDE is a code editor — it has no reason to request your camera, microphone, or location, so the browser is told upfront that those requests are denied. This is least-privilege at the platform layer: even if a third-party script or a future feature tried, the browser says no.
Cross-origin isolation — Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp
This is the strict pair, and the one that costs something. Together they give the page cross-origin isolation: other origins can't hold a live reference to your window, and your page refuses to load any cross-origin subresource unless that resource explicitly opts in (via a Cross-Origin-Resource-Policy header or CORS). What it buys is protection against a whole class of cross-origin read attacks — the Spectre family — and a stronger guarantee that the code running in your tab is only the code you shipped.
The price is real, and I paid it. COEP require-corp breaks anything cross-origin that doesn't opt in: a font, a CDN script, an analytics pixel, an embed. I have personally debugged a COEP/CORP failure on this site where a legitimate resource silently failed to load and the page just looked broken, with no error pointing at the header. The fix is discipline, not a weaker header: serve everything you can same-origin, and for anything genuinely cross-origin, make sure it sends Cross-Origin-Resource-Policy or is fetched in CORS mode. If you set COEP and something breaks, check the resource's headers before you blame your code — that is the single most useful debugging rule in this whole post.
Cloudflare also appends Report-To / NEL (Network Error Logging) headers automatically. They're telemetry about failed requests, not a header I configured — I leave them alone.
What I deliberately left out
No Content-Security-Policy — yet. This is the honest part. CSP is the most powerful header on this list and the easiest to get wrong. A bad CSP breaks your product: blocked scripts, dead embeds, templates that silently stop working. A missing CSP breaks nothing. NitroIDE loads user code, templates, CDN libraries, and embeds — I want to test a CSP against every one of those before enforcing it. It is on the list; it is not shipped on a hope.
No X-Frame-Options. It is legacy. The modern replacement is CSP's frame-ancestors, which lands with the CSP work above. Shipping the old header now would be theater — one more line that looks like security without the policy behind it.
Verify it yourself
Headers are claims. Verify them the way I did, in one command:
curl -sI https://nitroide.com/
You should see all six headers in the response. For a second opinion, run the URL through a header scanner (OWASP's Secure Headers Project and securityheaders.com both grade the same set) — but read the findings, don't chase the letter grade. A scanner will flag the missing CSP; you now know why it's missing, and "missing on purpose, tracked" beats "added to silence the scanner, breaking the product."
The copy-paste version
| Header | Value |
|---|---|
| Strict-Transport-Security | max-age=31536000; includeSubDomains |
| X-Content-Type-Options | nosniff |
| Referrer-Policy | strict-origin-when-cross-origin |
| Permissions-Policy | camera=(), microphone=(), geolocation=() |
| Cross-Origin-Opener-Policy | same-origin |
| Cross-Origin-Embedder-Policy | require-corp |
Six headers, one Transform Rule, zero code changes. The first four are free — no downside, no debugging, no reason not to ship them today. The last two are the ones that earn their keep: set them, then budget an hour for the cross-origin resource that will surprise you.
Further reading: MDN's guides on HSTS, Permissions-Policy, and cross-origin isolation with COOP/COEP, plus the OWASP Secure Headers Project.
Try it
I build NitroIDE — a browser-based IDE where your code never leaves your machine: nitroide.com
⭐ If you enjoyed this, star NitroIDE on GitHub: github.com/nitroideofficial/nitroide
Frequently asked questions
Why does NitroIDE have no Content-Security-Policy?
CSP is the most powerful header and the easiest to get wrong. A bad CSP breaks the product: blocked scripts, dead embeds, templates that silently stop working. A missing CSP breaks nothing. NitroIDE loads user code, templates, CDN libraries, and embeds — the policy will ship once it is tested against all of them, not to silence a scanner.
Why no X-Frame-Options header?
X-Frame-Options is legacy. The modern replacement is CSP's frame-ancestors directive, which lands with the CSP work. Shipping the old header now would be theater — one more line that looks like security without the policy behind it.
What does COEP require-corp break?
Any cross-origin subresource that doesn't opt in: a font, a CDN script, an analytics pixel, an embed. The fix is discipline, not a weaker header: serve everything you can same-origin, and for anything genuinely cross-origin, make sure it sends Cross-Origin-Resource-Policy or is fetched in CORS mode.
Why does the HSTS header leave out preload?
Preload is a one-way door — once you're on the browser preload list, every subdomain must serve valid HTTPS forever, and removal takes months. One year + includeSubDomains is the strong posture without the irreversible commitment.