Skip to content

fix(boot): ask the dashboard's own site, and tell it apart from the default vhost (#1140) - #1155

Merged
VijitSingh97 merged 1 commit into
develop-v2from
fix/1140-boot-probe
Aug 20, 2026
Merged

fix(boot): ask the dashboard's own site, and tell it apart from the default vhost (#1140)#1155
VijitSingh97 merged 1 commit into
develop-v2from
fix/1140-boot-probe

Conversation

@VijitSingh97

Copy link
Copy Markdown
Collaborator

Closes #1140.

The boot health gate's fast half dialled https://localhost/ and accepted any status but 000. Its
comment said localhost is a listed site, so the answer traverses the real dashboard vhost. That
holds only while dashboard.host is unset — generate_caddyfile adds localhost inside
is_appliance && [ -z "$DASHBOARD_HOST" ]. Pin the host, which is documented and supported, and
localhost drops out: the probe reaches Caddy's empty default vhost, which answers 200 with no
body
, and "Caddy is running" passes as "the dashboard serves" — on the gate that decides whether
an A/B update lives, and that #1065 reboots on.

Both halves change together, and that is the point

Tightening the acceptance alone would convert the false pass into a permanent failure on exactly
the machines this issue is about: with the host pinned there is genuinely nothing at localhost to
believe. That is the same false-RED shape as #1151, on the same gate. The issue's own preferred
option had this in it — see the design comment.

  • Ask the right site. gate_url reads HOST_IP, DASHBOARD_SECURE and HOST_PORT from the
    .env this boot has just rendered. HOST_IP is what the render uses as its first site address,
    so the probe and the site list cannot disagree — one rule, not a second copy of the expansion.
  • Recognise the right answer. gate_answer_is_dashboard accepts a body, or the 401 a locked
    dashboard sends, and rejects the default vhost's empty 200. Deliberately not a 401 check: an
    empty dashboard password is the documented default (# OPT-IN — an empty password (the default) leaves it open), and that machine answers 200, so a 401 gate would fail forever on every
    no-login appliance.
  • Dial it correctly. dashboard.host is validated as "a hostname or IP address" and explicitly
    allows colons, so HOST_IP can be an IPv6 literal. Measured against curl 8.7 rather than reasoned
    about: curl refuses one in --resolve outright — Couldn't parse CURLOPT_RESOLVE entry — and an
    unbracketed literal in the URL reads the port as part of the address. Either way the request
    fails and the gate never passes. So gate_target_url brackets a literal, and gate_resolve_spec
    emits --resolve only for names, where it is what keeps the dial on loopback instead of making a
    boot-time health probe hostage to the box resolving its own mDNS name.
  • code/size are cleared each iteration, so a curl that prints nothing cannot leave the previous
    round's answer standing.

The review found a gate that lies, inside this change

The first version replaced the old single-line regex with two independent greps. The doctor half,
grep -qE 'pithead doctor --json', matched this file's own header comment on line 15 — so the
one assertion whose job is "both signals gate the same mark-good" stayed green with the doctor call
deleted from the commit condition outright. That is #852 restored, with the test that exists to
prevent it still passing.

Fixed by moving the pairing itself above the sourceable boundary as gate_ready, driven at tier 1
with a stubbed pithead. BOOT_DOCTOR_JSON becomes a plain variable for the same reason
BOOT_FAIL_COUNT is one — the suite points it somewhere writable. All five helpers now live above
the boundary, so they are driven rather than asserted about.

Tier 1

bash tests/stack/run.sh2722 passed, 0 failed (base 2700).

Mutations — eight named, eight run

# mutation result
M1 acceptance widened back to "any status but 000" 2 red
M2 probe target hard-coded back to localhost 4 red
M3 the fail-safe HOST_IP fallback removed 1 red
M4 acceptance narrowed to 401-only (the fix the issue first recommended) 2 red
M5 IPv6 bracketing dropped from gate_target_url 1 red
M6 --resolve emitted for IP literals too 2 red
M7 the doctor half deleted from the commit gate — the #852 regression the old grep could not see 1 red
M8 the probe half widened back inside gate_ready 1 red

M4 is worth calling out: the suite goes red on the fix originally recommended, because the no-login
case is now encoded as a requirement.

Not done

  • The curl invocation itself is still below the sourceable boundary and has no tier-1 coverage —
    only the values it is built from and the decision it feeds. The honest place for the rest is
    leg 4 of the battery.
  • No --phase update battery run on this branch. This changes pithead-boot, which the bench
    is the real gate for, and that has not been run here.
  • The render is untouched: localhost still leaves the site list when dashboard.host is pinned.
    This makes the probe honest about that rather than changing what the box serves — the vhost
    question belongs with The appliance's certificate and its Caddy site list are built by different rules, and the certificate is never re-minted #1132, where the certificate SAN list is decided by a different rule.

…efault vhost (#1140)

The boot health gate's fast half dialled `https://localhost/` and accepted any status
but `000`. Its comment said localhost is a listed site so the answer traverses the
real dashboard vhost. That holds only while `dashboard.host` is unset —
`generate_caddyfile` adds localhost inside `is_appliance && [ -z "$DASHBOARD_HOST" ]`.
Pin the host, which is documented and supported, and localhost drops out: the probe
reaches Caddy's empty default vhost, which answers 200 with no body, and "Caddy is
running" passes as "the dashboard serves" on the gate that decides whether an A/B
update lives and that #1065 reboots on.

Both halves change together, and that is the point. Tightening the acceptance alone
would convert the false pass into a permanent FAILURE on exactly the machines this is
about, because with the host pinned there is genuinely nothing at localhost to
believe — the same false-RED shape as #1151.

- ask the right site: `gate_url` reads HOST_IP, DASHBOARD_SECURE and HOST_PORT from
  the .env this boot has just rendered. HOST_IP is what the render uses as its first
  site address, so the probe and the site list cannot disagree — one rule, not a
  second copy of the expansion.
- recognise the right answer: `gate_answer_is_dashboard` accepts a body, or the 401 a
  locked dashboard sends, and rejects the default vhost's empty 200. Deliberately not
  a 401 check — an empty dashboard password is the documented default, and that box
  answers 200, so a 401 gate would fail forever on every no-login appliance.
- dial it correctly: `dashboard.host` is validated as "a hostname or IP address" and
  explicitly allows colons, so HOST_IP can be an IPv6 literal. curl refuses one in
  `--resolve` outright — it rejects the whole option with "Couldn't parse
  CURLOPT_RESOLVE entry" — and an unbracketed literal in the URL reads the port as
  part of the address. Either way the request fails and the gate never passes, so
  `gate_target_url` brackets a literal and `gate_resolve_spec` emits `--resolve` only
  for names, where it keeps the dial on loopback instead of depending on the box
  resolving its own mDNS name. Measured against curl 8.7, not reasoned about.
- `code`/`size` are cleared each iteration, so a curl that prints nothing cannot leave
  the previous round's answer standing.

And the pairing itself (#852) moves into `gate_ready`. The review of the first version
of this branch caught that its replacement assertion could not fail: splitting the old
single-line regex into two greps let the doctor half match this file's own HEADER
COMMENT, so the test stayed green with the doctor call deleted from the commit
condition outright — a gate that lies, inside the change that exists to remove one.
As a function the pairing is driven at tier 1 with a stubbed `pithead`, and deleting
either half now goes red.

All the helpers live above pithead-boot's sourceable boundary so they are driven
rather than asserted about. `BOOT_DOCTOR_JSON` becomes a plain variable for the same
reason `BOOT_FAIL_COUNT` is one — the suite points it somewhere writable.
@VijitSingh97

Copy link
Copy Markdown
Collaborator Author

Merging with Build image (dashboard) red. Pre-existing and unrelated — #1156. Three util-linux advisories published between 04:43 and 13:32 today, on a branch nobody touched; the pinned python:3.11-slim digest is already the newest published one, so there is nothing to bump to.

Build + scan the appliance rootfs is now green on this PR, which it was not before the rebase — that is #1154 landing.

@VijitSingh97
VijitSingh97 merged commit 9edf794 into develop-v2 Aug 20, 2026
15 of 16 checks passed
@VijitSingh97
VijitSingh97 deleted the fix/1140-boot-probe branch August 20, 2026 13:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant