From 56e55ff488e2c9295d868a5a6fe7ee5b8cdfeb73 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 10 May 2026 21:54:38 +0200 Subject: [PATCH] feat(infra): add production Caddyfile Reverse proxy for the Familienarchiv host, validated against Caddy 2. Includes both vhosts (production and staging), the Gitea vhost, and: - HSTS, X-Content-Type-Options, Referrer-Policy headers on every site - "-Server" header strip to hide the Caddy version - /actuator/* responds 404 on both archive vhosts (defense in depth for Spring Boot's management endpoints) X-Frame-Options is intentionally not set in Caddy: Spring Security configures frame-options SAMEORIGIN for the in-app PDF preview iframe; a DENY header here would conflict. Refs #497. Co-Authored-By: Claude Sonnet 4.6 --- infra/caddy/Caddyfile | 63 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 infra/caddy/Caddyfile diff --git a/infra/caddy/Caddyfile b/infra/caddy/Caddyfile new file mode 100644 index 00000000..f32b1f2f --- /dev/null +++ b/infra/caddy/Caddyfile @@ -0,0 +1,63 @@ +# Caddyfile for the Familienarchiv host. +# +# Caddy runs on the host (not in a container) and reverse-proxies into +# the docker compose stacks bound to 127.0.0.1. +# +# Naming convention for ports (also documented in docker-compose.prod.yml): +# production: backend 8080, frontend 3000 +# staging: backend 8081, frontend 3001 +# gitea: 3005 +# +# Security headers and the /actuator block apply to both archive vhosts. +# X-Frame-Options is deliberately NOT set here: Spring Security configures +# frame-options SAMEORIGIN (for the in-app PDF preview iframe). Setting +# DENY in Caddy would conflict. + +(security_headers) { + header { + Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" + X-Content-Type-Options "nosniff" + Referrer-Policy "strict-origin-when-cross-origin" + -Server + } +} + +(block_actuator) { + # Defense in depth: even if management.endpoints.web.exposure.include grows + # in application.yaml, /actuator/* is unreachable externally. The internal + # Prometheus scrape (future) talks to the backend directly on the docker + # network, not via Caddy. + @actuator path /actuator/* + respond @actuator 404 +} + +archiv.raddatz.cloud { + import security_headers + import block_actuator + + handle /api/* { + reverse_proxy 127.0.0.1:8080 + } + + handle { + reverse_proxy 127.0.0.1:3000 + } +} + +staging.raddatz.cloud { + import security_headers + import block_actuator + + handle /api/* { + reverse_proxy 127.0.0.1:8081 + } + + handle { + reverse_proxy 127.0.0.1:3001 + } +} + +git.raddatz.cloud { + import security_headers + reverse_proxy 127.0.0.1:3005 +}