- Caddy request_body { max_size 12MB } in both Caddyfiles — a hard
ceiling replacing Caddy's multi-GB default; 12MB clears the
orchestrator's 10 MiB user-route read. Validated with caddy validate.
- email-inbound router gets an explicit DefaultBodyLimit::max(1MB):
the public unauthenticated webhook previously rode Axum's 2MB
extractor default; tightened so a flood can't force large
allocation + JSON parse per request.
Admin / execute handlers keep Axum's 2MB extractor default (already
bounded); the user-route path keeps its explicit 10 MiB manual read.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
109 lines
3.7 KiB
Caddyfile
109 lines
3.7 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 {
|
|
# Audit 2026-06-11 (H-1) — hard request-body ceiling at the proxy.
|
|
# Without this, Caddy forwards bodies up to its multi-GB default to
|
|
# picloud. 12 MB sits just above the orchestrator's 10 MiB user-route
|
|
# read so legitimate large invokes pass; admin / email handlers are
|
|
# bounded far tighter by their own Axum extractor limits.
|
|
request_body {
|
|
max_size 12MB
|
|
}
|
|
|
|
# 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
|
|
}
|
|
}
|