hub v0.86.0 — Copy works without revealing, and every copy branch reports itself
Found by the operator, in the way that matters: it cost a real login.
The v0.84.0 Console access card shipped its Copy button DISABLED until a Reveal.
Clicking it did nothing, silently, so the clipboard kept whatever was already in
it — another host's console password from an earlier reveal. That got pasted into
demo-hp's PVE login, which failed with no explanation: the box logged a plain
`password check failed for user (root)`, the credential was never at fault, and
nothing on screen said the copy had not happened.
A copy button that silently no-ops is worse than no copy button. The operator
cannot tell "copied" from "did nothing", and the stale value left behind is a
VALID secret for a DIFFERENT machine — so the failure looks like a stale
credential and sends you diagnosing the wrong thing.
Copy now works without revealing, and that is the safer default rather than a
concession: the secret goes straight to the clipboard and never renders on
screen, so it cannot be shoulder-surfed or caught in a screenshot. Reveal remains
for when it must be read.
Three silent-failure branches closed, all in the same eight-line function:
- not yet revealed -> was a disabled no-op; now fetches and copies
- navigator.clipboard absent -> was silently skipped; now shows it and says why
- writeText() REJECTED -> promise was ignored, so the operator believed it
copied; now shows it and reports the refusal
The success path names the host ("Copied demo-hp-bb76ea's root@pam password"),
because the clipboard is fleet-wide and every box has a different console
password — "copied" alone cannot say for WHICH box, which is the confusion that
produced the incident.
One retrieval path, shared: the endpoint is defined once (data-reveal-url) and
read back with getAttribute, so Copy cannot drift onto a different, unaudited URL
than Reveal. Server-side is unchanged — both buttons hit the same CSRF-gated
endpoint and both write the same recovery_credential_revealed event, which is
correct: the register records accesses, and a copy is an access.
Tests 566 -> 568. Red-proof: re-adding `disabled` reproduces the shipped bug.
This commit is contained in:
@@ -1,3 +1,44 @@
|
||||
## v0.86.0 — Copy works without revealing, and every copy branch reports itself (2026-07-31)
|
||||
|
||||
**Found by the operator, in the way that matters: it cost a real login.** The v0.84.0 Console access
|
||||
card shipped its **Copy button `disabled` until a Reveal**. Clicking it did nothing, silently — so the
|
||||
clipboard kept whatever was already in it, which was **another host's console password** from an
|
||||
earlier reveal. That got pasted into demo-hp's PVE login, which failed with no explanation. The box
|
||||
logged a plain `password check failed for user (root)`; the credential was never at fault, and there
|
||||
was nothing on screen to say the copy had not happened.
|
||||
|
||||
**A copy button that silently no-ops is worse than no copy button**, because the operator has no way
|
||||
to distinguish "copied" from "did nothing" — and the stale value it leaves behind is a *valid secret
|
||||
for a different machine*, so the resulting failure looks like a stale-credential problem and sends you
|
||||
diagnosing the wrong thing.
|
||||
|
||||
**Copy now works without revealing — and that is the safer default, not a concession.** The secret
|
||||
goes straight to the clipboard and never renders on screen, so it cannot be shoulder-surfed or caught
|
||||
in a screenshot. Reveal is still there for when you need to read it (typing at a console).
|
||||
|
||||
**Three silent-failure branches closed, all in the same eight-line function:**
|
||||
|
||||
| Branch | Was | Now |
|
||||
|---|---|---|
|
||||
| Not yet revealed | button `disabled`, click = no-op | fetches and copies |
|
||||
| `navigator.clipboard` absent (insecure context) | `if (navigator.clipboard)` → silently skipped | shows the password instead and says why |
|
||||
| `writeText()` promise REJECTED (permission / no user gesture) | promise ignored — the operator believes it copied | shows the password instead and reports the refusal |
|
||||
|
||||
**The success path now names the host:** *"✓ Copied demo-hp-bb76ea's root@pam password to the
|
||||
clipboard."* The clipboard is fleet-wide and every box has a different console password, so "copied"
|
||||
alone cannot say copied for *which* box — precisely the confusion that produced the incident.
|
||||
|
||||
**One retrieval path, shared.** `fetchConsolePassword` is used by both buttons, and the endpoint is
|
||||
defined once (the `data-reveal-url` attribute) and read back with `getAttribute`, so Copy cannot drift
|
||||
onto a different — unaudited — URL than Reveal. A test asserts the URL appears exactly once.
|
||||
Server-side nothing changed: both buttons hit the same CSRF-gated endpoint and both write the same
|
||||
`recovery_credential_revealed` event, which is correct — the register records **accesses**, and a copy
|
||||
is an access.
|
||||
|
||||
Tests 566 → 568, both pinning this regression: the Copy button must not ship `disabled`, and every
|
||||
outcome branch must carry a message. Red-proof: re-adding `disabled` reproduces the shipped bug and
|
||||
turns the first test red.
|
||||
|
||||
## v0.85.0 — Network card: a host's addresses are visible at last (2026-07-31)
|
||||
|
||||
**Pairs with agent v0.119.0 and is useless without it** — the agent is what reports the addresses.
|
||||
|
||||
Reference in New Issue
Block a user