type: decision
status: active
timestamp: 2026-06-20
tags: [decisions, security, headers, csp, hsts]
status: active
timestamp: 2026-06-20
tags: [decisions, security, headers, csp, hsts]
Security headers — strict CSP via _headers + dual CI audit
Strict CSP/HSTS/Permissions-Policy via CF _headers from oriz-kit
Security headers — strict CSP via _headers + dual CI audit
Decision
Every family site ships a strict security-headers preset via a
_headers file at
the project root, sourced from @chirag127/oriz-kit. Cloudflare
Pages reads the file at deploy and applies the rules at the edge.
Every PR runs two auditors in parallel:
- securityheaders.com — Scott Helme’s rubric (headers grade)
- Mozilla Observatory — TLS + cookies + redirects + headers (comprehensive)
PR fails if either score drops below A.
Why
- Default-deny is the only safe stance. A strict CSP without
unsafe-evalor wildcard origins is the single most effective XSS mitigation; it has to ship by default, not as an opt-in per site. - Config-as-code via
_headers— auditable in git, no Cloudflare UI clicks, ships in the kit. Every site copies the preset; per-site additions allowed but never weakening. - Dual audit catches drift. securityheaders.com grades headers alone; Mozilla Observatory adds TLS + cookies + redirect chains. Either alone leaves a category unchecked.
- Both are free, no card. Aligns with no-card-on-file.
- Layered with the code-quality stack — same philosophy: defensive layering, fail loud, no silent drift.
The locked preset
The kit’s _headers template:
/*
Content-Security-Policy: default-src 'self'; script-src 'self' 'wasm-unsafe-eval' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https://*.imagekit.io https://*.cloudinary.com; connect-src 'self' https://api.oriz.in https://*.firebaseio.com https://firestore.googleapis.com https://*.knock.app https://vitals.vercel-insights.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(self), usb=()
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-site
connect-src includes the family’s known endpoints:
api.oriz.in,
Firestore, Knock, and
Vercel Speed Insights.
Per-site additions are allowed but must be reviewed.
Implications
Architecture
@chirag127/oriz-kitships the_headerspreset undertemplates/_headers. Each site’s CI either copies the file or extends it via per-site_headers.append.- The preset registers HSTS preload (
max-age=63072000= 2 years,includeSubDomains,preload) —oriz.inapex must be submitted to https://hstspreload.org/ once across the family (it covers all subdomains). - Permissions-Policy whitelist is minimal: only
payment=(self)for Razorpay flows. Camera / mic / geolocation / accelerometer are off everywhere — sites that need them flip the relevant directive in their per-site_headers.
CI gate
- Per-repo
ci.ymladds two jobs after the Cloudflare Pages preview deploy:security-headers-grade— calls securityheaders.com API, parses JSON for grade, fails if notAorA+.mozilla-observatory— runsnpx @mdn/mdn-http-observatoryagainst the preview hostname, fails if score < 85.
- API key for securityheaders.com lives in Doppler and syncs to GitHub Actions; Mozilla Observatory CLI needs no key.
What “below A” means
- securityheaders.com grade = A or A+ ? pass; any other grade fails.
- Mozilla Observatory score = 85 (= grade A) ? pass; below fails.
- Score drops — i.e. a PR is blocked only if it makes things worse. Pre-existing site state is the floor; PRs cannot weaken it. (Lifting all sites to A is a one-time push, not a per-PR blocker.)
What we don’t do
- No
unsafe-evalanywhere in CSP. Sites that need eval-style patterns (e.g. live MDX preview) sandbox them in an iframe. - No
*wildcard origins inconnect-src/img-src. Specific hosts only. - No paid auditors — Hardenize, Probely, Detectify all paid.
- No Cloudflare Transform Rules UI — config-as-code via
_headersis auditable; UI clicks drift silently. - No HSTS rollback path — preload list removal takes weeks; a site that breaks HTTPS stays broken. The kit’s preset is reviewed before family-wide adoption.
Cross-refs
- Cloudflare _headers service entry
- securityheaders.com service entry
- Mozilla Observatory service entry
- security services index
- Cloudflare Pages — host that reads
_headers - Code-quality stack decision
- Per-repo CI workflows decision
- Multi-provider auth — App Check + reCAPTCHA also gate Firestore
- Doppler — secrets sync
- No card-on-file rule