security: add CSP + standard security headers in nginx (demo + prod) #37

Open
opened 2026-05-03 07:31:51 +00:00 by padreug · 0 comments
Owner

Summary

nginx.conf.example and the per-host nginx vhosts (server-deploy hosts/*/) don't set any security response headers today. A handful are zero-effort and high-leverage, especially given the prvkey-in-memory model: anything that limits attacker surface area before #11's centralised SigningService lands is worth doing.

Risk: MEDIUM — defensive hardening, not closing a known active exploit.

Headers to add

Header Value Why
Content-Security-Policy strict default-src 'self', script-src 'self', img-src 'self' data: blob: https:, connect-src 'self' wss: https:, frame-ancestors 'none' Limits XSS blast radius — without inline-script allowances, an injected <script> from user content can't run
X-Frame-Options DENY Prevents clickjacking the standalone PWAs into hostile iframes
X-Content-Type-Options nosniff Stops content-type sniffing (e.g. file uploads served as scripts)
Referrer-Policy strict-origin-when-cross-origin Don't leak full URLs (with ?token=…!) to outbound links
Permissions-Policy camera=(), microphone=(), geolocation=() (lift per-app where needed) Default-deny browser APIs we don't use
Strict-Transport-Security max-age=31536000; includeSubDomains HSTS — only on hosts we actually serve over HTTPS

Acceptance criteria

  • nginx.conf.example ships a documented add_header block (or a reusable include snippet) that sets the headers above.
  • The deploy modules (server-deploy/modules/services/webapp-standalone.nix and the main webapp module's nginx config) inherit those headers.
  • CSP is strict by default (no 'unsafe-inline', no 'unsafe-eval'). Document any allowance with a comment explaining why.
  • Validate against demo using https://securityheaders.com after the next deploy — aim for at least an A grade.

Notes / known constraints

  • Vue 3 + vite production builds don't emit inline scripts by default, so script-src 'self' should hold.
  • Tailwind 4 may emit inline styles in some build configurations — verify and add style-src 'self' 'unsafe-inline' only if necessary; document why.
  • The ?token=… cross-subdomain auth relay in the chakra hub is a referrer-leak risk if Referrer-Policy is permissive — strict-origin-when-cross-origin keeps the path/query out of the referrer header on cross-origin requests.

Out of scope

  • Subresource Integrity (SRI) hashes — separate issue if we ever inline external CDN scripts.
  • COOP/COEP for shared-array-buffer features — not currently needed.
  • App-level CSP nonces — only required if we end up needing inline script tags.
## Summary `nginx.conf.example` and the per-host nginx vhosts (server-deploy `hosts/*/`) don't set any security response headers today. A handful are zero-effort and high-leverage, especially given the prvkey-in-memory model: anything that limits attacker surface area before #11's centralised SigningService lands is worth doing. **Risk: MEDIUM** — defensive hardening, not closing a known active exploit. ## Headers to add | Header | Value | Why | |---|---|---| | `Content-Security-Policy` | strict default-src 'self', script-src 'self', img-src 'self' data: blob: https:, connect-src 'self' wss: https:, frame-ancestors 'none' | Limits XSS blast radius — without inline-script allowances, an injected `<script>` from user content can't run | | `X-Frame-Options` | `DENY` | Prevents clickjacking the standalone PWAs into hostile iframes | | `X-Content-Type-Options` | `nosniff` | Stops content-type sniffing (e.g. file uploads served as scripts) | | `Referrer-Policy` | `strict-origin-when-cross-origin` | Don't leak full URLs (with `?token=…`!) to outbound links | | `Permissions-Policy` | `camera=(), microphone=(), geolocation=()` (lift per-app where needed) | Default-deny browser APIs we don't use | | `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | HSTS — only on hosts we actually serve over HTTPS | ## Acceptance criteria - `nginx.conf.example` ships a documented `add_header` block (or a reusable `include` snippet) that sets the headers above. - The deploy modules (`server-deploy/modules/services/webapp-standalone.nix` and the main webapp module's nginx config) inherit those headers. - CSP is **strict by default** (no `'unsafe-inline'`, no `'unsafe-eval'`). Document any allowance with a comment explaining why. - Validate against demo using https://securityheaders.com after the next deploy — aim for at least an A grade. ## Notes / known constraints - Vue 3 + vite production builds don't emit inline scripts by default, so `script-src 'self'` should hold. - Tailwind 4 may emit inline styles in some build configurations — verify and add `style-src 'self' 'unsafe-inline'` only if necessary; document why. - The `?token=…` cross-subdomain auth relay in the chakra hub is a referrer-leak risk if `Referrer-Policy` is permissive — `strict-origin-when-cross-origin` keeps the path/query out of the referrer header on cross-origin requests. ## Out of scope - Subresource Integrity (SRI) hashes — separate issue if we ever inline external CDN scripts. - COOP/COEP for shared-array-buffer features — not currently needed. - App-level CSP nonces — only required if we end up needing inline script tags.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/webapp#37
No description provided.