hub v0.84.0 — break-glass console credential on the host page
The credential existed and was not reachable when it was wanted. Every box has
had a strong random root@pam password since TASK G1, vaulted in the hub at day 0
and used for real during the sshd incident — but the only way to read it back was
a hand-written curl carrying the global operator key, a secret kept out-of-band.
In practice the PVE web console on a demo box felt locked.
The host page grows a Console access card: presence + username + set_at by
default, Reveal fetches the plaintext on demand for 60 s with a Copy button.
Masking clears the JS variable, and also fires on a second click and on
visibilitychange. A host with nothing vaulted says so, and says why.
The secret is NEVER rendered into the page, and that constraint shapes the
change. The render path uses a new store.GetHostRecoveryMeta whose struct and
SELECT both omit the secret column, so it is structurally incapable of carrying
one. The plaintext crosses the wire only in the response to POST
/hosts/{id}/reveal-recovery-credential (Cache-Control: no-store, CSRF-gated at
the ServeHTTP level; POST precisely so that gate applies and so no secret is
retrievable by URL alone). Deliberately NOT the customer page's data-secret
widget, which embeds the plaintext on every load.
A delivered reveal writes one recovery_credential_revealed event on the host's
customer timeline (info, source hub, Hungarian) via SaveEvent alone — no
dispatcher, nobody emailed, the log_tail_requested shape. Two reveals write two
events: the register records accesses, not states. A 404 is not an access. An
unbound host reveals fine and writes no event; the [INFO] hub line, carrying the
username and a length only, is then the record.
The global-key API path is untouched by design — it is the route for when the
hub UI itself is broken, and coupling it to the session layer would delete the
independence that makes it a fallback.
Recorded as a real trade: the hub session password alone now unlocks console root
fleet-wide, where retrieval previously also needed the global key. Accepted for a
single-operator, HU-geo-fenced hub that already stores these passwords in
plaintext at rest (CONTEXT.md ruling S-4). The plaintext-at-rest half is filed as
R-133 — every hub DB backup is a fleet-wide console-credential dump.
Tests 550 -> 559; four red-proofs (page leak, audit event, CSRF gate, route
order) each run, observed failing, and reverted. The route-order proof is a seam
test driving ServeHTTP: a handler-level test cannot see that defect, because the
handler is correct and simply never runs.
This commit is contained in:
@@ -39,13 +39,26 @@ Layer 2 heals within ~60s with nobody logged in. If you just saw the KEXINIT-res
|
||||
retry SSH. If it's back, check the hub for a `mgmt_plane_healed` warning and go to §4 (find the cause).
|
||||
|
||||
### 3.1 If SSH is still dead — reach the PVE web console (break-glass)
|
||||
1. **Retrieve the vaulted root@pam password** (operator/global key):
|
||||
1. **Retrieve the vaulted root@pam password.**
|
||||
|
||||
**Primary route — the hub UI (hub v0.84.0):** hub → **Hosts** → the host → the **Console access**
|
||||
card → **Reveal**. The password shows for 60 s with a Copy button. The card also states which user
|
||||
it is for and when it was last vaulted, so a stale copy is visible before you try it. A host with
|
||||
nothing vaulted says so and says why, instead of showing a blank. Needs only the hub login
|
||||
password — no out-of-band key.
|
||||
|
||||
**Fallback — the global-key curl. Use this when the hub UI itself is unavailable** (that is the
|
||||
whole reason it still exists, and why it deliberately has no dependency on the hub's session
|
||||
layer):
|
||||
```
|
||||
curl -s -H "Authorization: Bearer <GLOBAL-KEY out-of-band>" \
|
||||
https://hub.felhom.eu/api/v1/admin/hosts/<host_id>/recovery-credential
|
||||
# → {"host_id":"…","username":"root@pam","password":"…","set_at":"…"}
|
||||
```
|
||||
(A per-host key is refused here by design — only the operator key reads a console password back.)
|
||||
|
||||
Either route is an **operator action against a customer's box**, and the UI route records it: see
|
||||
§5.
|
||||
2. **Open the PVE web UI** `https://<host-ip>:8006`, log in `root@pam` + that password, realm
|
||||
*Linux PAM standard authentication*.
|
||||
3. **Node → Shell** (xterm.js root console). Or navigate directly:
|
||||
@@ -87,6 +100,17 @@ Fix the cause; the three layers are the safety net, not the cure.
|
||||
wipes `state.json`, so step 4b re-randomizes — re-run the set-and-vault afterward. Background: on each
|
||||
full reinstall this is exactly why `root@pam` "kept changing" until it was pinned.
|
||||
- **The password is never logged or filed** — it exists only in the hub vault (retrieve as in §3.1).
|
||||
- **Revealing it through the hub UI writes a customer-visible event line** (hub v0.84.0):
|
||||
`recovery_credential_revealed`, info severity, on that host's customer timeline, in Hungarian —
|
||||
*„Az üzemeltető lekérte a géped konzolos hozzáférési jelszavát (távoli hibaelhárítás)."* Nobody is
|
||||
emailed (no dispatcher call), and the register records **accesses**, so two reveals write two
|
||||
lines. Deliberate, for a product sold on data sovereignty; it mirrors how a remote log pull is
|
||||
already recorded. An **unbound** host has no customer to tell — there the hub `[INFO]` log line is
|
||||
the only record. The global-key curl path writes no event.
|
||||
- **The hub login password now unlocks console root on every managed box.** Before v0.84.0,
|
||||
retrieval also required the global API key. Accepted for a single-operator, HU-geo-fenced hub that
|
||||
already holds these passwords in plaintext at rest — a convenience trade, and a real one. The
|
||||
plaintext-at-rest half (every hub DB backup is a fleet-wide console-credential dump) is **R-133**.
|
||||
- **Least-privilege console user** instead of root@pam was probed as a non-blocking observation: PVE
|
||||
has historically gated the node Shell to `root@pam`, so G1 ships the proven root@pam path. Revisit if
|
||||
PVE later allows a scoped realm user to open a node Shell.
|
||||
|
||||
Reference in New Issue
Block a user