#!/bin/bash # SVOS front-door acceptance matrix (eshpfi configs/svos-front/), 2026-10-05. # Run from a MESH peer. Feed the variables on stdin ahead of this file so no credential touches an argv: # pw=$(secret get nh3-svos/basic-auth-vuong) # { printf 'BASE=https://svos.phasefinal.com\nCURLX="--resolve svos.phasefinal.com:443:100.64.0.9"\nCRED=%s\nINV=%q\nIDX_SHA=%s\n' \ # "Vuong:$pw" "$()" "$()" # cat configs/svos-front/acceptance-matrix.sh; } | ssh infra-ops@ 'bash -s' # Positive control (2026-10-05): against the previous Caddyfile (no public shell, try_files fallback) it reports # 16 FAIL, so it can see the behaviours it checks. # expects: BASE, CURLX (extra curl args), CRED (user:pass), INV (lines "path sha256"), IDX_SHA (sha256 of index.html) pass=0; fail=0 ok() { pass=$((pass+1)); printf 'PASS %s\n' "$1"; } bad() { fail=$((fail+1)); printf 'FAIL %s :: %s\n' "$1" "$2"; } req() { # $1 name, $2 auth(y/n), then curl args; sets CODE REALM CT CC SHA local name=$1 auth=$2; shift 2 local hdr=$(mktemp) body=$(mktemp) if [ "$auth" = y ]; then CODE=$(curl -s $CURLX -K <(printf 'user = "%s"\n' "$CRED") -D "$hdr" -o "$body" -w '%{http_code}' "$@") else CODE=$(curl -s $CURLX -D "$hdr" -o "$body" -w '%{http_code}' "$@"); fi REALM=$(grep -ci '^www-authenticate: Basic realm="SVOS"' "$hdr"); CT=$(grep -i '^content-type:' "$hdr" | tr -d '\r' | cut -d' ' -f2-) CC=$(grep -i '^cache-control:' "$hdr" | tr -d '\r' | cut -d' ' -f2-); SHA=$(sha256sum "$body" | cut -d' ' -f1); rm -f "$hdr" "$body" } gated() { req "$1" n "${@:2}"; [ "$CODE" = 401 ] && [ "$REALM" = 1 ] && ok "$1 -> 401 realm" || bad "$1" "code=$CODE realm=$REALM"; } echo "== unauthenticated: exact inventory is public, hash-preserved" while read -r p h; do req "U1 GET $p" n "$BASE$p"; [ "$CODE" = 200 ] && [ "$SHA" = "$h" ] && ok "U1 GET $p 200 sha ok cc=[$CC]" || bad "U1 GET $p" "code=$CODE sha=$SHA"; done <<< "$INV" req "U1 GET /sw.js" n "$BASE/sw.js"; [ "$CODE" = 200 ] && echo "$CT" | grep -q javascript && [ "$CC" = no-cache ] && ok "U1 GET /sw.js 200 ct=[$CT] cc=[$CC]" || bad "U1 /sw.js" "code=$CODE ct=$CT cc=$CC" req "U2 HEAD /index.html" n -I "$BASE/index.html"; [ "$CODE" = 200 ] && ok "U2 HEAD /index.html 200" || bad "U2" "code=$CODE" gated "U3 POST /index.html" -X POST "$BASE/index.html" gated "U4 GET /assets/missing.js" "$BASE/assets/missing.js" gated "U5 GET /" "$BASE/" gated "U5 GET /status" "$BASE/status" for p in /api/health /health /openapi.json /verbs/ping /verbs/pins; do gated "U6 GET $p" "$BASE$p"; done gated "U7 GET /assets/../api/health (path-as-is)" --path-as-is "$BASE/assets/../api/health" gated "U8 GET /index.html/" "$BASE/index.html/" req "U9 GET /INDEX.HTML" n "$BASE/INDEX.HTML"; [ "$CODE" = 404 ] || [ "$CODE" = 401 ] && ok "U9 /INDEX.HTML -> $CODE (no bytes served)" || bad "U9" "code=$CODE" gated "U10 GET /%2e%2e/etc/passwd" --path-as-is "$BASE/%2e%2e/etc/passwd" gated "U11 GET /icons/" "$BASE/icons/" echo "== authenticated" for p in / /status; do req "A1 GET $p" y -H 'Sec-Fetch-Dest: document' "$BASE$p"; [ "$CODE" = 200 ] && [ "$SHA" = "$IDX_SHA" ] && [ "$CC" = no-cache ] && ok "A1 GET $p -> index.html cc=[$CC]" || bad "A1 $p" "code=$CODE sha=$SHA cc=$CC"; done req "A3 GET /assets/missing.js" y "$BASE/assets/missing.js"; [ "$CODE" = 404 ] && [ "$SHA" != "$IDX_SHA" ] && ok "A3 missing asset -> 404 (not index)" || bad "A3" "code=$CODE" req "A3b GET /assets/missing.js as document" y -H 'Sec-Fetch-Dest: document' "$BASE/assets/missing.js"; [ "$CODE" = 404 ] && ok "A3b missing asset as document -> 404" || bad "A3b" "code=$CODE" req "A4 GET /icons/" y "$BASE/icons/"; [ "$CODE" = 404 ] && ok "A4 /icons/ -> 404" || bad "A4" "code=$CODE" req "A5 GET /nowhere (no doc headers)" y "$BASE/nowhere"; [ "$CODE" = 404 ] && ok "A5 /nowhere -> 404" || bad "A5" "code=$CODE" req "A6 GET /nowhere Sec-Fetch-Dest" y -H 'Sec-Fetch-Dest: document' "$BASE/nowhere"; [ "$CODE" = 200 ] && [ "$SHA" = "$IDX_SHA" ] && ok "A6 /nowhere document -> index.html" || bad "A6" "code=$CODE" req "A7 GET /nowhere Accept html" y -H 'Accept: text/html,application/xhtml+xml' "$BASE/nowhere"; [ "$CODE" = 200 ] && [ "$SHA" = "$IDX_SHA" ] && ok "A7 /nowhere Accept html -> index.html" || bad "A7" "code=$CODE" for p in /api/health /health; do req "A8 GET $p" y "$BASE$p"; [ "$REALM" = 0 ] && ok "A8 GET $p passes Caddy auth (no Basic challenge) -> upstream $CODE" || bad "A8 $p" "code=$CODE realm=$REALM"; done req "A9 POST /index.html" y -X POST "$BASE/index.html"; [ "$CODE" = 405 ] || [ "$CODE" = 404 ] && ok "A9 POST /index.html authed -> $CODE" || bad "A9" "code=$CODE" echo "== $pass pass, $fail fail"