Caddyfile 4.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. # FrankenPHP Caddyfile for the api container.
  2. # Serves Slim from public/ on :8081.
  3. {
  4. frankenphp
  5. order php_server before file_server
  6. auto_https off
  7. admin off
  8. # SEC_REVIEW F25: trust ONLY loopback as an XFF rewriter by default.
  9. # The previous `trusted_proxies static private_ranges` honoured
  10. # X-Forwarded-For from any RFC1918 peer. Combined with the wide
  11. # /internal/* CIDR gate, a neighbouring container on the same docker
  12. # bridge could forge `REMOTE_ADDR=127.0.0.1` via XFF and pass the
  13. # network check. The bundled docker-compose stack has no real proxy
  14. # in front of the api, so loopback-only is the correct default.
  15. #
  16. # Production deployments behind a real reverse proxy override the
  17. # `TRUSTED_PROXIES` env var to that proxy's CIDR — for example:
  18. # TRUSTED_PROXIES="10.0.0.5/32"
  19. # The default keeps `remote_ip` matchers below evaluating against the
  20. # real TCP peer regardless of any XFF header.
  21. servers {
  22. trusted_proxies static {$TRUSTED_PROXIES:127.0.0.1/32 ::1/128}
  23. }
  24. }
  25. :8081 {
  26. root * /app/public
  27. encode zstd gzip
  28. # ── Security headers (M14) ──────────────────────────────────────────
  29. # Applied to every response. The api serves JSON + the OpenAPI YAML +
  30. # the /api/docs viewer; everything else is locked down.
  31. header {
  32. # Identify ourselves as little as possible.
  33. -Server
  34. -X-Powered-By
  35. X-Content-Type-Options "nosniff"
  36. # The api doesn't render its own pages except /api/docs which is a
  37. # single embedded viewer; SAMEORIGIN is the conservative default
  38. # that still allows future same-origin embedding.
  39. X-Frame-Options "SAMEORIGIN"
  40. Referrer-Policy "strict-origin-when-cross-origin"
  41. Permissions-Policy "geolocation=(), microphone=(), camera=()"
  42. # SEC_REVIEW F59: modern cross-origin isolation headers.
  43. # - COOP `same-origin` isolates the docs viewer / any
  44. # future api-rendered page from cross-origin popups.
  45. # - CORP `same-origin` blocks external pages from loading
  46. # the api JSON via cross-origin <script>/<img>/<link>
  47. # tags (defence in depth; CORS already gates fetch).
  48. # - X-Permitted-Cross-Domain-Policies `none` blocks legacy
  49. # Adobe Flash / Acrobat cross-domain.xml lookups.
  50. Cross-Origin-Opener-Policy "same-origin"
  51. Cross-Origin-Resource-Policy "same-origin"
  52. X-Permitted-Cross-Domain-Policies "none"
  53. }
  54. # HSTS: prod-only. Setting it in dev would lock you out of plain-HTTP
  55. # localhost on the same hostname (sticky for 1 year). Gate strictly.
  56. @prod expression `{env.APP_ENV} == "production"`
  57. header @prod Strict-Transport-Security "max-age=31536000; includeSubDomains"
  58. # CSP: docs viewer needs RapiDoc from jsDelivr + inline styles + the
  59. # try-it-now feature posting to /api/v1/*. Everything else is JSON.
  60. @docs path /api/docs /api/v1/openapi.yaml
  61. header @docs Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data: https://cdn.jsdelivr.net; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
  62. @not_docs not path /api/docs /api/v1/openapi.yaml
  63. header @not_docs Content-Security-Policy "default-src 'none'; frame-ancestors 'none'; base-uri 'none'"
  64. # Internal jobs API: only callable from loopback by default
  65. # (SEC_REVIEW F25). The bundled sidecar scheduler joins the api's
  66. # network namespace via `network_mode: "service:api"`, so its calls
  67. # arrive on 127.0.0.1. Host-cron Option A in the SPEC also targets
  68. # localhost. Production topologies that genuinely need wider
  69. # reachability set `INTERNAL_CIDR_ALLOWLIST` for the PHP middleware
  70. # AND mirror the same CIDR list in this matcher (operators must
  71. # patch the Caddyfile or override via a custom config).
  72. # The PHP layer also enforces this (InternalNetworkMiddleware) —
  73. # Caddy is the first line of defence.
  74. @internal {
  75. path /internal/*
  76. remote_ip 127.0.0.1/32 ::1/128
  77. }
  78. handle @internal {
  79. php_server
  80. }
  81. @external_internal_blocked {
  82. path /internal/*
  83. not remote_ip 127.0.0.1/32 ::1/128
  84. }
  85. respond @external_internal_blocked 404
  86. php_server
  87. }