fix(boot): ask the dashboard's own site, and tell it apart from the default vhost (#1140) - #1155
Merged
Merged
Conversation
…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
force-pushed
the
fix/1140-boot-probe
branch
from
August 20, 2026 13:44
c37210e to
64a8be4
Compare
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 Build + scan the appliance rootfs is now green on this PR, which it was not before the rebase — that is #1154 landing. |
6 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1140.
The boot health gate's fast half dialled
https://localhost/and accepted any status but000. Itscomment said localhost is a listed site, so the answer traverses the real dashboard vhost. That
holds only while
dashboard.hostis unset —generate_caddyfileadds localhost insideis_appliance && [ -z "$DASHBOARD_HOST" ]. Pin the host, which is documented and supported, andlocalhost 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.
gate_urlreadsHOST_IP,DASHBOARD_SECUREandHOST_PORTfrom the.envthis boot has just rendered.HOST_IPis 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.
gate_answer_is_dashboardaccepts a body, or the401a lockeddashboard 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 everyno-login appliance.
dashboard.hostis validated as "a hostname or IP address" and explicitlyallows colons, so
HOST_IPcan be an IPv6 literal. Measured against curl 8.7 rather than reasonedabout: curl refuses one in
--resolveoutright —Couldn't parse CURLOPT_RESOLVE entry— and anunbracketed 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_urlbrackets a literal, andgate_resolve_specemits
--resolveonly for names, where it is what keeps the dial on loopback instead of making aboot-time health probe hostage to the box resolving its own mDNS name.
code/sizeare cleared each iteration, so a curl that prints nothing cannot leave the previousround'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 theone 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 1with a stubbed
pithead.BOOT_DOCTOR_JSONbecomes a plain variable for the same reasonBOOT_FAIL_COUNTis one — the suite points it somewhere writable. All five helpers now live abovethe boundary, so they are driven rather than asserted about.
Tier 1
bash tests/stack/run.sh→ 2722 passed, 0 failed (base 2700).Mutations — eight named, eight run
localhostgate_target_url--resolveemitted for IP literals toogate_readyM4 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
curlinvocation 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.
--phase updatebattery run on this branch. This changespithead-boot, which the benchis the real gate for, and that has not been run here.
localhoststill leaves the site list whendashboard.hostis 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.