# 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 ``) 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 `