From d8fd4ff655e0168f2c871d52f0307c64ba106986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Thu, 11 Jun 2026 07:56:30 +0200 Subject: [PATCH] security: bind Caddy admin API to loopback, split metrics to :2020 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The flagship Caddy ran its admin API on 0.0.0.0:2019, reachable by every container on the Docker network. The admin API can rewrite routes and proxy targets, so a journal/planner RCE could repoint traffic with no further auth. (The brouter-host Caddyfile already binds admin to localhost; the flagship didn't.) The admin endpoint also served Prometheus metrics (global `metrics` option → admin endpoint), and Prometheus scrapes caddy:2019 cross-container — so admin couldn't just move to loopback without breaking metrics. Split them: - admin localhost:2019 (loopback only). All reloads are in-container (`docker compose exec caddy caddy reload`) so they use this endpoint unaffected. - a dedicated `:2020` server exposes the read-only metrics handler; prometheus.yml now scrapes caddy:2020. Validated with `caddy validate` + `caddy adapt` against caddy:2: admin.listen=localhost:2019, srv on :2020 carries the metrics handler, the :443 site is unchanged. Co-Authored-By: Claude Fable 5 --- infrastructure/caddy/Caddyfile | 15 +++++++++++++-- infrastructure/prometheus/prometheus.yml | 4 +++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/infrastructure/caddy/Caddyfile b/infrastructure/caddy/Caddyfile index 9282f65..38fb647 100644 --- a/infrastructure/caddy/Caddyfile +++ b/infrastructure/caddy/Caddyfile @@ -1,6 +1,17 @@ { - metrics - admin 0.0.0.0:2019 + # Admin API on loopback only. It can reconfigure the proxy, so it must + # not be reachable by other containers on the Docker network (a journal + # / planner RCE could otherwise rewrite routes). Reloads run in-container + # (`docker compose exec caddy caddy reload`) over this loopback endpoint, + # matching infrastructure/brouter-host/Caddyfile. + admin localhost:2019 +} + +# Read-only Prometheus metrics on a dedicated network-reachable port, +# split off the admin endpoint above so scraping doesn't require exposing +# the admin API. Prometheus scrapes caddy:2020 (see prometheus.yml). +:2020 { + metrics /metrics } (security_headers) { diff --git a/infrastructure/prometheus/prometheus.yml b/infrastructure/prometheus/prometheus.yml index c64726b..f89bd6a 100644 --- a/infrastructure/prometheus/prometheus.yml +++ b/infrastructure/prometheus/prometheus.yml @@ -41,7 +41,9 @@ scrape_configs: - job_name: "caddy" static_configs: - - targets: ["caddy:2019"] + # Caddy's admin API is loopback-only; metrics are served on a + # dedicated port (see infrastructure/caddy/Caddyfile). + - targets: ["caddy:2020"] # Self-monitoring. Prometheus did not scrape its own /metrics, so TSDB # health (compaction failures, WAL corruption, head series, retention