From 378502524c153ec3c40cd5d038356729bcba6dc2 Mon Sep 17 00:00:00 2001 From: GDS K S Date: Sun, 13 Sep 2026 22:27:40 -0500 Subject: [PATCH] feat: add Content-Security-Policy and opt-in HSTS, fix healthz routing Ships the two security headers explicitly deferred from the earlier production-readiness batch (securityHeadersMiddleware's own prior doc comment): - Content-Security-Policy is now sent unconditionally, script-src locked to 'self' with no unsafe-inline/unsafe-eval. This required extracting web/index.html's inline theme-flash-prevention script into web/public/theme-init.js first, verified via a real npm run build that the built dist/index.html has no inline script left. - Strict-Transport-Security is opt-in via APP_ENABLE_HSTS (default off): this control plane's own HTTP server never terminates TLS itself, so it can't tell from a request alone whether the browser saw a trusted ACME certificate or the self-signed internal-issuer default, and HSTS on a self-signed deployment turns a certificate warning into a hard lockout. Documented in domains-and-ingress.md next to the ACME/self-signed TLS explanation. Also fixes a real routing bug found while live-verifying this: the control plane's top-level mux only forwarded the "/api/" prefix to the API router, so a plain GET /healthz (what an orchestrator or load balancer actually probes) fell through to the SPA catch-all and got a 200 with the dashboard's index.html body instead of {"status":"ok"}. Extracted the mux composition into composeMux so this routing precedence is directly unit-tested instead of only reachable through rootHandler's full dependency graph. Verified live: built with -tags embedweb, ran against an isolated docker:dind daemon, confirmed GET /healthz, GET /, GET /theme-init.js, and GET /api/v1/brand all return the expected body and headers. What this doesn't do: - No nonce/hash-based CSP: style-src keeps 'unsafe-inline' because @xterm/xterm injects its own