Back to Hub
ENGINEERING •
By Yash Panchal · Last updated:

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

HeaderValue
Strict-Transport-Securitymax-age=31536000; includeSubDomains
X-Content-Type-Optionsnosniff
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=()
Cross-Origin-Opener-Policysame-origin
Cross-Origin-Embedder-Policyrequire-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.