Caddyfile: serve the homepage via handle blocks (fix the /admin/ 401)

Caddy SORTS top-level directives, so a 'rewrite' runs before 'basicauth'.
The rewrite changed /admin/ to /admin.html, and the path-based auth matchers
then saw /admin.html (which matched the family 'not path /admin/*' block, not
the operator '/admin/*' block) — so the operator password was checked against
the family hash and 401'd.

Serve each page from its own 'handle' block (which preserves order, so the
auth is checked on the original path before any file rewrite):
- handle /admin* : operator (frank) only, try_files /admin.html
- handle @data   : shared data + assets, family (dank) OR operator (frank), so
  the admin page's fetches of services.json/status.json reuse the operator
  credential
- handle         : everything else (the user page), family (dank)
This commit is contained in:
Frank
2026-08-23 18:33:54 -07:00
parent 25af3d9fdf
commit c7ced90aca
2 changed files with 55 additions and 17 deletions
+34 -10
View File
@@ -46,17 +46,41 @@
# --- Homepage (password protected) --- # --- Homepage (password protected) ---
dankclaw.franksdank.cloud { dankclaw.franksdank.cloud {
@admin path /admin/*
@user not path /admin/*
basicauth @user {
dank REPLACE_WITH_HASH
}
basicauth @admin {
frank REPLACE_WITH_ADMIN_HASH
}
rewrite @admin /admin.html
root * /srv/homepage root * /srv/homepage
file_server
# Operator-only admin page (Q19). Auth is checked on the original path,
# before any rewrite, so the path matcher is reliable. Caddy sorts a
# top-level 'rewrite' before 'basicauth'; that would change the path and
# break the path-based auth matchers. 'handle' preserves order.
handle /admin* {
basicauth {
frank REPLACE_WITH_ADMIN_HASH
}
try_files /admin.html
file_server
}
# Shared data + assets: the family OR the operator, so both pages can load
# them (the browser reuses the page's credential for these fetches). A
# 'handle' inline matcher is a single token, so the multi-path match is a
# named matcher.
@data path /services.json /status.json /app.js /style.css /deployed.js
handle @data {
basicauth {
dank REPLACE_WITH_HASH
frank REPLACE_WITH_ADMIN_HASH
}
file_server
}
# Everything else (the user page): the family (Q18).
handle {
basicauth {
dank REPLACE_WITH_HASH
}
try_files /index.html
file_server
}
} }
# --- Legacy name: the media host has no Caddy of its own anymore --- # --- Legacy name: the media host has no Caddy of its own anymore ---
+21 -7
View File
@@ -16,8 +16,12 @@ setup() {
} }
@test "the admin basicauth is scoped to /admin/ (Q19)" { @test "the admin basicauth is scoped to /admin/ (Q19)" {
grep -qE "@admin[[:space:]]+path[[:space:]]+/admin/" "$CADDY" # The admin page is a 'handle /admin*' block with the operator (frank)
grep -qE "basicauth[[:space:]]+@admin" "$CADDY" # credential. Caddy sorts a top-level 'rewrite' before 'basicauth', which
# would change the path and break the path-based auth matcher; 'handle'
# preserves order, so the auth is checked on the original path.
grep -qE "handle[[:space:]]+/admin\*" "$CADDY"
grep -qE "frank[[:space:]]+REPLACE_WITH_ADMIN_HASH" "$CADDY"
} }
@test "the homepage serves /srv/homepage" { @test "the homepage serves /srv/homepage" {
@@ -25,9 +29,19 @@ setup() {
grep -q "file_server" "$CADDY" grep -q "file_server" "$CADDY"
} }
@test "the /admin/ path is rewritten to admin.html" { @test "the /admin/ page serves admin.html and / serves index.html" {
# The matcher token follows the directive (rewrite @admin ...), per the # Each page is served by its own 'handle' block via 'try_files' (which
# Caddyfile matcher docs. A leading '@admin rewrite' is parsed as a # serves the named file without a top-level 'rewrite' that Caddy would
# one-liner matcher DEFINITION and re-defines @admin (invalid). # sort before the basicauth).
grep -qE "rewrite[[:space:]]+@admin[[:space:]]+/admin.html" "$CADDY" grep -qE "try_files[[:space:]]+/admin.html" "$CADDY"
grep -qE "try_files[[:space:]]+/index.html" "$CADDY"
}
@test "the shared data + assets accept both the family and the operator" {
# The admin page (frank) and the user page (dank) both fetch the shared
# data (services.json, status.json) and assets. The browser reuses the
# page's credential for these, so the block must accept both users.
grep -qE "@data[[:space:]]+path[[:space:]]+/services\.json[[:space:]]+/status\.json" "$CADDY"
grep -qE "handle[[:space:]]+@data" "$CADDY"
grep -qE "dank[[:space:]]+REPLACE_WITH_HASH" "$CADDY"
} }