# FrankenPHP Caddyfile for the api container. # Serves Slim from public/ on :8081. { frankenphp order php_server before file_server auto_https off admin off # SEC_REVIEW F25: trust ONLY loopback as an XFF rewriter by default. # The previous `trusted_proxies static private_ranges` honoured # X-Forwarded-For from any RFC1918 peer. Combined with the wide # /internal/* CIDR gate, a neighbouring container on the same docker # bridge could forge `REMOTE_ADDR=127.0.0.1` via XFF and pass the # network check. The bundled docker-compose stack has no real proxy # in front of the api, so loopback-only is the correct default. # # Production deployments behind a real reverse proxy override the # `TRUSTED_PROXIES` env var to that proxy's CIDR — for example: # TRUSTED_PROXIES="10.0.0.5/32" # The default keeps `remote_ip` matchers below evaluating against the # real TCP peer regardless of any XFF header. servers { trusted_proxies static {$TRUSTED_PROXIES:127.0.0.1/32 ::1/128} } } :8081 { root * /app/public encode zstd gzip # ── Security headers (M14) ────────────────────────────────────────── # Applied to every response. The api serves JSON + the OpenAPI YAML + # the /api/docs viewer; everything else is locked down. header { # Identify ourselves as little as possible. -Server -X-Powered-By X-Content-Type-Options "nosniff" # The api doesn't render its own pages except /api/docs which is a # single embedded viewer; SAMEORIGIN is the conservative default # that still allows future same-origin embedding. X-Frame-Options "SAMEORIGIN" Referrer-Policy "strict-origin-when-cross-origin" # SEC_REVIEW F61: extended deny-list for browser features the # api never serves. Aligned with the ui Caddyfile so a browser # loading a doc page (RapiDoc viewer) sees the same hardening. # The api doesn't use clipboard-write so it stays denied here # too (the ui keeps it on its same-origin default for the # "copy raw token" button). Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), bluetooth=(), camera=(), clipboard-read=(), clipboard-write=(), display-capture=(), encrypted-media=(), fullscreen=(), gamepad=(), geolocation=(), gyroscope=(), hid=(), idle-detection=(), interest-cohort=(), magnetometer=(), microphone=(), midi=(), payment=(), picture-in-picture=(), screen-wake-lock=(), serial=(), speaker-selection=(), usb=(), web-share=(), xr-spatial-tracking=()" # SEC_REVIEW F59: modern cross-origin isolation headers. # - COOP `same-origin` isolates the docs viewer / any # future api-rendered page from cross-origin popups. # - CORP `same-origin` blocks external pages from loading # the api JSON via cross-origin