Files
bztmon-site/SECURITY.md
T
jwright c1db5cec86 M4: security pass — strict CSP, header split, build-time scan
All JS moved to external /site.js → script-src 'self' with no inline JS,
hashes or eval. Full header set via nginx (CSP, nosniff, frame-deny,
referrer, permissions, COOP/CORP); HSTS stays at the CF edge. Shared
headers include avoids the location add_header reset footgun. Build-time
secret/inline-script/third-party scan gate. SECURITY.md documents posture.
2026-06-17 17:12:57 +10:00

2.8 KiB

Security posture

This site is meant to demonstrate the discipline it advertises. Security is treated as acceptance criteria, not polish.

Attack surface

  • Static output — no server runtime, no database, no user input, no forms.
  • Served by nginx-unprivileged (uid 101, read-only root filesystem, all Linux capabilities dropped, no service-account token) on Kubernetes.
  • Exposed outbound-only via a Cloudflare Tunnel — no open inbound ports, a single public hostname, no catch-all.

Headers

Split by where they belong:

Origin (nginx, ships in the image — nginx/security-headers.conf)

Header Value
Content-Security-Policy default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; worker-src 'self'; manifest-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy camera/mic/geo/payment/usb… all denied
Cross-Origin-Opener-Policy same-origin
Cross-Origin-Resource-Policy same-origin

Edge (Cloudflare dashboard) — HSTS, HTTP→HTTPS redirect, SSL Full, Bot Fight, rate-limiting. HSTS ships without includeSubDomains/preload initially because *.bztmon.com resolves to the WAN and the subdomain form would brick non-public hosts.

CSP notes

  • script-src 'self' with zero inline scripts. All JS (pre-paint theme, toggle, scroll reveal) lives in one external /site.js. No unsafe-inline, no unsafe-eval, no hashes to maintain.
  • style-src allows 'unsafe-inline' — the one conscious exception. Shiki's dual-theme syntax highlighting emits per-token CSS custom-properties as inline style attributes; hashing them is impractical (they vary per page). The security-critical directive (script-src) stays strict. Everything else is self-hosted: fonts are system stacks (zero font requests), no third-party scripts, so SRI is moot.
  • CSP is emitted as a real HTTP header (not <meta>) so frame-ancestors is honoured and header scanners can see it.

Build-time gate

npm run scan (scripts/check-build.sh) fails the build on:

  • secret-like strings in dist/
  • any inline <script> (would break the strict CSP)
  • third-party <script>/<link> origins

Runs in CI before the image is built.

Targets (verified post-deploy)

  • securityheaders.com → A+
  • Mozilla Observatory → A/A+ (small deduction expected for style-src 'unsafe-inline')
  • Lighthouse ≥ 95 across Performance / Accessibility / Best Practices / SEO
  • CSP: zero console violations in a real browser (incl. code blocks + diagrams)

Reporting

Found something? Email the address on the site.