bug(caddy): respond @actuator 404 swallowed by catch-all handle; /actuator/health returns 302 #512

Closed
opened 2026-05-11 16:34:15 +02:00 by marcel · 0 comments
Owner

Summary

The (block_actuator) Caddyfile snippet emits respond @actuator 404 at the top level of each archive vhost. But each vhost also contains a catch-all handle { reverse_proxy ... } block. In Caddy's directive order, handle is mutually exclusive — once a handle matches, the request never reaches a top-level respond. The catch-all handle always matches /actuator/*, so the request hits the backend and Spring Security 302s to /login.

Reproduction

$ curl -sS -o /dev/null -w "HTTP %{http_code}\n" https://staging.raddatz.cloud/actuator/health
HTTP 302   # expected: 404

Impact

  • Workflow smoke test fails on the /actuator/health → 404 assertion.
  • Defense-in-depth posture is broken: a probe can distinguish between actuator paths (302 to /login from Spring Security) and non-actuator paths the backend doesn't know (different response). Nora's defense-in-depth promise from PR #499 is effectively unmet.

Fix

Move the actuator block inside its own handle directive so it participates in the mutually-exclusive route table. handle blocks are sorted by path specificity (most specific first), so /actuator/* takes precedence over the catch-all handle:

(block_actuator) {
    handle /actuator/* {
        respond 404
    }
}

Discovered

Direct curl against the up-and-running staging stack while debugging #510.

## Summary The `(block_actuator)` Caddyfile snippet emits `respond @actuator 404` at the top level of each archive vhost. But each vhost also contains a catch-all `handle { reverse_proxy ... }` block. In Caddy's directive order, `handle` is mutually exclusive — once a `handle` matches, the request never reaches a top-level `respond`. The catch-all `handle` always matches `/actuator/*`, so the request hits the backend and Spring Security 302s to `/login`. ## Reproduction ``` $ curl -sS -o /dev/null -w "HTTP %{http_code}\n" https://staging.raddatz.cloud/actuator/health HTTP 302 # expected: 404 ``` ## Impact - **Workflow smoke test fails** on the `/actuator/health → 404` assertion. - **Defense-in-depth posture is broken**: a probe can distinguish between actuator paths (302 to /login from Spring Security) and non-actuator paths the backend doesn't know (different response). Nora's defense-in-depth promise from PR #499 is effectively unmet. ## Fix Move the actuator block inside its own `handle` directive so it participates in the mutually-exclusive route table. `handle` blocks are sorted by path specificity (most specific first), so `/actuator/*` takes precedence over the catch-all `handle`: ```caddyfile (block_actuator) { handle /actuator/* { respond 404 } } ``` ## Discovered Direct curl against the up-and-running staging stack while debugging #510.
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#512