diff --git a/packaging/caddy/Caddyfile.template b/packaging/caddy/Caddyfile.template index b9f9ad86..12bfbf9e 100644 --- a/packaging/caddy/Caddyfile.template +++ b/packaging/caddy/Caddyfile.template @@ -60,6 +60,34 @@ reverse_proxy 127.0.0.1:8080 } + # Public allowlist — paths that MUST be reachable without basic_auth + # so the first-run wizard can bootstrap before any credential exists, + # and so monitoring tools can scrape liveness without holding creds. + # + # This list mirrors the PUBLIC_PATHS frozenset in + # src/hal0/api/middleware/auth.py — keep them in sync. The `path` + # matcher is exact-match (no trailing-slash or prefix expansion), which + # matches the `request.url.path in PUBLIC_PATHS` check on the API side. + # + # This block MUST stay above the default `handle {}` below — Caddy + # evaluates `handle` blocks in source order and the first match wins, + # so listing `@public` later would let basic_auth swallow the wizard + # probes (see issue #28). + @public path /api/health/system \ + /api/status \ + /api/metrics \ + /api/features \ + /api/install/state \ + /api/install/complete \ + /api/config/urls \ + /api/auth/status \ + /api/auth/login \ + /api/auth/logout \ + /v1/models + handle @public { + reverse_proxy 127.0.0.1:8080 + } + # Dashboard + management API — Caddy basicauth at the edge. Forwards # the identity as X-Forwarded-Email so the hal0 API trusts the user # as admin (see hal0.api.middleware.auth precedence). diff --git a/tests/harness/installer-test.sh b/tests/harness/installer-test.sh index 2d91d300..fe3ce72d 100755 --- a/tests/harness/installer-test.sh +++ b/tests/harness/installer-test.sh @@ -243,11 +243,20 @@ else HAL0_HOSTNAME=hal0-harness.local HAL0_TLS_EMAIL=harness@hal0.test \ HAL0_NO_PROBE=1 HAL0_PLAIN=1 \ sudo -E bash "${REPO_ROOT}/installer/install.sh" --auth=basic --no-start >"${LOG4}" 2>&1; then + # Regression for #28: the rendered Caddyfile must carry the + # @public matcher + `handle @public` block BEFORE the default + # basicauth handler, else the first-run wizard's pre-auth + # /api/install/state + /api/config/urls probes 401 and the + # SPA can't bootstrap. Asserting both halves (matcher + handle) + # so a future edit that drops one without the other is caught. if [[ -f /etc/hal0/Caddyfile ]] && grep -q basicauth /etc/hal0/Caddyfile \ - && grep -q HAL0_AUTH_ENABLED=1 /etc/hal0/api.env; then - add_row "auth-basic" "pass" "$(since_ms "${start}")" "Caddyfile + api.env auth wiring rendered" + && grep -q HAL0_AUTH_ENABLED=1 /etc/hal0/api.env \ + && grep -q '@public path' /etc/hal0/Caddyfile \ + && grep -q 'handle @public' /etc/hal0/Caddyfile \ + && grep -q '/api/install/state' /etc/hal0/Caddyfile; then + add_row "auth-basic" "pass" "$(since_ms "${start}")" "Caddyfile + api.env auth wiring rendered (incl. @public allowlist)" else - add_row "auth-basic" "fail" "$(since_ms "${start}")" "Caddyfile or api.env auth flag missing post-install" + add_row "auth-basic" "fail" "$(since_ms "${start}")" "Caddyfile or api.env auth flag missing post-install (check @public allowlist for #28)" fi else rc=$?