Files
PiCloud/caddy/Caddyfile
MechaCat02 9067225945 fix(audit-2026-06-11/H-A1+A2+A3+M07-06+07+08+09): Caddy security-header layer
Adds defense-in-depth HTTP headers to the dev and prod Caddyfiles. CSP,
X-Frame-Options, Permissions-Policy, and Cache-Control: no-store apply
to the dashboard SPA (/admin/*) and admin API (/api/v1/admin/*). nosniff
and Referrer-Policy apply to every response (a sane default). HSTS is
unconditional in prod.

User-route responses (the catch-all `handle`) deliberately get NO CSP —
user scripts own their own response headers by design.

The CSP / X-Frame-Options / Cache-Control / Permissions-Policy headers
use the `?` operator ("set only if not already present") so application
responses with their own restrictive policy — notably the audit C-2
file-download handler, which sends a sandboxed CSP — win over Caddy's
defaults.

This downgrades H-A4 (dashboard token in localStorage) from acute to
defense-in-depth: with this CSP and no inline JS in the dashboard,
XSS-based token exfiltration is no longer a one-step exploit.

Validated with `caddy validate` (docker caddy:2-alpine) for both files;
also `caddy fmt --overwrite`'d.

Audit ref: security_audit/07_http_cors_csrf_xss.md (H07-03/04/05 +
M07-06/07/08/09).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-11 20:27:47 +02:00

100 lines
3.3 KiB
Caddyfile

# PiCloud dev Caddyfile.
#
# Routing topology:
# /healthz, /version → picloud (liveness + version, k8s-friendly)
# /api/v1/admin/* → picloud (manager control plane)
# /api/v1/execute/{id} → picloud (orchestrator ID-based bypass)
# /api/* → 404 (unsupported / sunset API version)
# /admin/* → dashboard SPA (mounted with paths.base=/admin)
# everything else → picloud (orchestrator route table; user paths)
#
# The "everything else → picloud" rule is what makes user-defined paths
# like /greet possible. picloud's matcher answers them via the routes
# table, or returns 404 with a precise JSON error.
#
# When v2 of the API ships, add `handle /api/v2/admin/* { ... }` etc.
# alongside the v1 handles, before the catch-all `/api/*` 404.
#
# Audit 2026-06-11 (H-A1/2/3, M07-06/07/08/09): defense-in-depth headers.
# CSP / X-Frame-Options / Permissions-Policy / no-store land on the
# dashboard SPA and admin API; nosniff + Referrer-Policy land everywhere.
# User-route responses (catch-all `handle`) deliberately get NO CSP —
# user scripts own their own response headers by design.
# `?` operator = "default if missing" so a downstream response (e.g. the
# file-download endpoint with its own restrictive CSP) wins.
{
auto_https off
admin off
log {
output stdout
format console
}
}
:80 {
# Baseline headers applied to every response.
header {
X-Content-Type-Options "nosniff"
Referrer-Policy "no-referrer"
}
handle /healthz {
reverse_proxy picloud:8080
}
handle /version {
reverse_proxy picloud:8080
}
handle /api/v1/admin/* {
header {
?Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none'"
?X-Frame-Options "DENY"
?Cache-Control "no-store"
?Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=(), accelerometer=(), gyroscope=()"
}
reverse_proxy picloud:8080
}
handle /api/v1/execute/* {
reverse_proxy picloud:8080
}
handle /api/* {
respond 404 {
body "{\"error\":\"no such API version see /version for supported routes\"}"
close
}
}
# Dashboard SPA at /admin. Its internal Caddy serves files from /srv
# (root); we don't strip the prefix because SvelteKit was built with
# paths.base = '/admin' so the bundle's URLs already include it.
handle /admin/* {
header {
?Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"
?X-Frame-Options "DENY"
?Cache-Control "no-store"
?Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=(), accelerometer=(), gyroscope=()"
}
reverse_proxy dashboard:80
}
handle /admin {
# Bare /admin (no trailing slash) — let SvelteKit's SPA handle it.
header {
?Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'"
?X-Frame-Options "DENY"
}
reverse_proxy dashboard:80
}
# Everything else → picloud's user-route fallback. If no route
# matches, picloud responds 404 with a JSON body.
handle {
reverse_proxy picloud:8080
}
log {
output stdout
format console
}
}