__DOMAIN__ { encode gzip # Cap request bodies. Pool JSON is the largest legitimate payload and # tops out well under 1 MiB; cap at 1 MiB so abusive uploads (large # blobs to /admin/api/* or pathological websocket frames pretending to # be HTTP) get rejected at the edge before reaching uvicorn. request_body { max_size 1MB } # /admin/login is rate-limited at the app layer (rate_limit.py: # 10/min/IP). A Caddy-edge limiter would be defense in depth, but # would require the non-stock `caddy-ratelimit` plugin; we keep this # bootstrap stock-Caddy-compatible. # Security headers. CSP allows Google Fonts (used by style.css) and # WebSocket back to the same origin; everything else is self-only. # X-Frame-Options DENY prevents clickjacking the admin into an iframe. # HSTS pin (1y, includeSubDomains, preload) so once a browser has # talked HTTPS to this host it refuses HTTP downgrades; safe because # the host is HTTPS-only. header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" Referrer-Policy "strict-origin-when-cross-origin" Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=(), usb=()" Content-Security-Policy "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; script-src 'self'; connect-src 'self' wss://__DOMAIN__ ws://__DOMAIN__; frame-ancestors 'none'; base-uri 'self'; form-action 'self'" # Server header leaks Caddy version; strip it. -Server } reverse_proxy 127.0.0.1:8001 { # Pass real client IP downstream so app-layer rate-limit + audit # logs see the actual student IP (not 127.0.0.1). header_up X-Forwarded-For {http.request.remote.host} header_up X-Real-IP {http.request.remote.host} } }