Files
familienarchiv/infra/caddy/Caddyfile
Marcel 4c8a23ff14
All checks were successful
CI / Unit & Component Tests (pull_request) Successful in 5m33s
CI / OCR Service Tests (pull_request) Successful in 33s
CI / Backend Unit Tests (pull_request) Successful in 7m10s
CI / fail2ban Regex (pull_request) Successful in 1m55s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m42s
devops(caddy): add Grafana and GlitchTip vhosts
grafana.archiv.raddatz.cloud → 127.0.0.1:3003 (with security headers)
glitchtip.archiv.raddatz.cloud → 127.0.0.1:3002 (no security headers —
  GlitchTip manages its own; the Sentry SDK also POSTs here)

Requires A records for both subdomains pointing at the server before
the next `systemctl reload caddy`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:27:07 +02:00

100 lines
2.7 KiB
Caddyfile

# 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"
# Deny browser APIs the app does not use. Reduces blast radius of an
# XSS landing in a privileged origin: a payload cannot silently turn
# on the microphone or read geolocation.
Permissions-Policy "camera=(), microphone=(), geolocation=()"
-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.
#
# Why a `handle` block and not a top-level `respond @matcher`: each archive
# vhost has a catch-all `handle { reverse_proxy ... }` that matches every
# path including /actuator/*, and Caddy's `handle` blocks are mutually
# exclusive. Without our own `handle /actuator/*` the catch-all wins, the
# request is proxied to the backend, and Spring Security 302s to /login
# instead of Caddy returning 404. See #512.
handle /actuator/* {
respond 404
}
}
(access_log) {
# JSON access log for fail2ban. The jail at infra/fail2ban/familienarchiv.conf
# watches this file for 401 responses on /api/auth/login.
# Caddy auto-creates /var/log/caddy/ when running as the `caddy` system user.
log {
output file /var/log/caddy/access.log {
roll_size 10mb
roll_keep 14
}
format json
}
}
archiv.raddatz.cloud {
import security_headers
import block_actuator
import access_log
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
import access_log
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
}
grafana.archiv.raddatz.cloud {
import security_headers
reverse_proxy 127.0.0.1:3003
}
glitchtip.archiv.raddatz.cloud {
reverse_proxy 127.0.0.1:3002
}