security(rest): ride the pinned CA at runtime, and fail closed with repairs - #265
Merged
Conversation
…epairs Fixes #264. Every REST call site adopted `panel_rest_transport` when the pin landed — config flow, reauth, repairs, rotate_credentials — except the one that runs on every startup: the schema fetch inside connect() stayed on plaintext HTTP with the pin sitting unused in entry.data, which is the condition the library's transport warning named. The runtime client now takes the pinned context and the HTTPS port (span-panel-api 3.4.0), so a pinned entry makes zero plaintext bootstrap calls in steady state. Failing closed follows the library's own escalation rule: a matching fingerprint never convicts. A rotated CA — diagnosed by re-reading the advertised CA, plaintext deliberately — is the one terminal outcome, taking the existing guided re-pin repair. Under an unchanged CA the leaf probe splits the rest: a moved panel raises the existing address repair and keeps its promise to retry; a leaf the pin does not validate — a middlebox, or a certificate a clock reset pushed outside its window — raises its own non-persistent repair and retries, so the clock case heals itself and plaintext is never fallen back to; missing evidence just retries. The two same-fingerprint repairs supersede each other, since a panel cannot be both moved and intercepted. A stored anchor that cannot be read stops setup with a fixable repair that reuses the CA-change flow, instead of quietly downgrading the one call that runs unattended on every boot. A stored HTTPS port of 80 — which the library refuses under a context — ends in a clear error naming Reconfigure, and both port forms now refuse it at input. For the population the deferred pin serves — TLS behind NAT, a port forward, a proxy, never asked for a port by the gated CA step — Reconfigure now offers the HTTPS port on any pinned entry, probing against and persisting the submitted value, and the retry message for an unreachable pinned panel names the expected port and the remedy. Entry removal clears the whole CA repair family, including two pre-existing leakers.
… Home Assistant Each release of the test harness pins one exact Home Assistant, so its pin and the homeassistant pin are a single choice — the pyproject says so. The bump to 0.13.358 paired with 2026.9.0b0 against the pinned 2026.8.3, an unresolvable pair that CI's re-lock from PyPI reports as a sqlalchemy split (2.0.52 vs 2.0.51) while a stale local venv resolves nothing and notices nothing. 0.13.357 is the release that pins 2026.8.3.
The Supervisor-discovery host check reads the serial that decides whether the entry moves, and it read it over plaintext with the entry's anchor sitting unused — the one probe of a configured host that did not ride the pin. Since span-panel-api 3.4.1 exempts the status endpoint from the plaintext warning, nothing in the log would say so either; the probe now takes the entry's own transport, on the add-on's newly published TLS port when it published one, because a reallocated port is the very case this probe exists for. Unpinned entries keep the plaintext probe they always had. (Direct checks run in lieu of the worktree-incompatible uv hooks: pytest 1676 passed, ruff, mypy all clean.)
The release that stops a merely advertising, unconfigured panel from producing the plaintext-transport warning at every boot when discovery probes it. Pairs with the Supervisor-discovery probe moving onto the pin in the previous commit, which that release's warning exemption motivated.
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.
Fixes #264.
What
A pinned entry's schema read — the one REST call that runs unattended at every startup — ran over plaintext HTTP with the pin sitting unused in
entry.data, which is exactly what the library's transport warning reported. The runtime client now rides the pinned CA on the HTTPS port (span-panel-api 3.4.0, SpanPanel/span-panel-api#175), so a healthy pinned entry makes zero plaintext bootstrap calls in steady state and the warning is resolved rather than silenced.Failing closed, without convicting transients
The escalation mirrors the library's own rule: a matching fingerprint never escalates.
The two same-fingerprint repairs supersede each other, since a panel cannot be both moved and intercepted.
The population that was never asked
Entries pinned by the deferred migration path can have TLS living somewhere 443 is not — behind NAT, a port forward, a proxy — and the CA step's gating never asked them for a port. Two remedies: Reconfigure now offers the HTTPS port on any pinned entry (probed against and persisted), and the retry message for an unreachable pinned panel names the expected port and the remedy instead of blaming the panel.
Also
Verification
1674 passed / 1 skipped, ruff and mypy clean,
sync_translations.pygreen — run against the library branch. Every behavioral change was written test-first (tests/test_runtime_rest_transport.pyplus reconfigure flow tests). Reviewed adversarially twice; all findings resolved and re-verified.CI note: cannot pass until span-panel-api 3.4.0 is on PyPI (CI re-locks from PyPI); merge SpanPanel/span-panel-api#175 and release first. The commit was made from a linked worktree, where the repo's uv-based hooks cannot resolve the editable library path (the pyproject documents this); every hook's check was run directly instead.