Skip to content

Commit 02b2b24

Browse files
pithead-developer[bot]pithead-developer[bot]claude
authored
fix(restore): preserve archived dashboard auth and prove every secret category (#2585)
* fix(restore): preserve archived dashboard auth * fix(restore): trust dashboard auth by fingerprint, not a Caddy probe Restore now keeps the archived dashboard hash under the render path's own rule: only while its fingerprint matches dashboard.auth.password. A stale pair is dropped and rehashed instead of refusing the archive. The Caddy container probe added no protection (the archive also carries the password) and failed wherever Compose or Docker was unavailable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore(gitleaks): ignore superseded #2579 restore fixtures Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(restore): validate the archive against its own environment The read-only validation read the live .env, so an archived dashboard password that differed from the live render forced a Caddy rehash (and Docker) before the stage was even canonicalized. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(restore): say when the validation step would rehash Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(integration): prove every secret category across the lifecycle restore The lifecycle backup -> restore round-trip compared only the proxy token and onion addresses, so a restore that rehashed the dashboard login passed it and surfaced only when a failing run's rollback made the per-category check (#2579). Compare the same wallet, proxy, dashboard, RPC and onion fingerprints the rollback does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(restore): shape-check the kept dashboard hash; assert every kept secret Reviewers found openssl accepts a valid bcrypt base64 followed by junk, and the hermetic test sampled one key per category. Require exactly 80 base64 characters, compare every kept key byte-for-byte, cover an auto stratum password, and document the refusal and the archive trust model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(restore): keep a matching archived dashboard hash through the wizard too Operator ruling on #2579 over #2231: restore keeps the archived dashboard hash and fingerprint exactly while the fingerprint matches the restored password, and regenerates a stale pair. A hash that is not well-formed bcrypt (an older release's, or a damaged one) is now dropped and regenerated instead of refusing the archive. The appliance restore rows and the KVM restore leg assert the archived hash survives, which is what an archive pithead produced authenticates with. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(restore): the restore leg keeps the archived dashboard hash Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(restore): name the malformed-hash rows literally The suite refuses PASS labels that interpolate an unreviewed value; the loop's $cr_bad label tripped it in Shell CI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: pithead-developer[bot] <4933481+pithead-developer[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: pithead-developer[bot] <pithead-developer[bot]@users.noreply.github.com>
1 parent 70a0a21 commit 02b2b24

12 files changed

Lines changed: 124 additions & 30 deletions

‎.config/gitleaksignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,8 @@ fa2a05ad20af64dd0372816917048829de7dab20:dashboard/tests/frontend/workeradopt.te
77
fa2a05ad20af64dd0372816917048829de7dab20:dashboard/tests/frontend/workeradoptlogic.test.mjs:generic-api-key:24
88
fa2a05ad20af64dd0372816917048829de7dab20:tests/stack/test-worker-config.sh:generic-api-key:140
99
e55d839eb8bbd47a8a1567031c2269af26f17549:tests/stack/test-worker-config.sh:generic-api-key:140
10+
# Synthetic dashboard bcrypt/fingerprint restore fixtures in a superseded #2579 branch commit; the
11+
# current test derives them at runtime.
12+
ec572a4034faa029839a6a29d616718d1513cef8:tests/stack/test-cli-restore-hardening.sh:generic-api-key:135
13+
ec572a4034faa029839a6a29d616718d1513cef8:tests/stack/test-cli-restore-hardening.sh:generic-api-key:136
14+
ec572a4034faa029839a6a29d616718d1513cef8:tests/stack/test-cli-restore-hardening.sh:generic-api-key:172

‎docs/dev/appliance-release.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ is the only thing standing between the hand-written boot path and a fleet.
306306
| `boot` | EFI boot to userspace; first-boot wizard announces itself with a console token; wizard serves the token gate on `:80`; machine-id is STABLE across a plain reboot (#895 — an empty-baked id with no restore mechanism regenerates on every boot, worse than the bug it fixes) | 4 |
307307
| `update` | `/data` grew to the disk and slots did not (#784); bundle installs into the spare; spare boots; **an uncommitted update reverts on reboot**; a committed update persists; **after the commit, the page served comes from the NEW dashboard image** (marker baked into the image and read back over HTTP — the tag never changes, so "containers run" proves nothing about staleness, #798); an operator can roll back off a committed version; **host identity (SSH host-key fingerprint, machine-id) survives the A/B swap** (#894/#895 — both live on `/data`, untouched by a slot swap). Then leg 4, **the dashboard OS-update action end-to-end** (#976): the machine is provisioned through the wizard's real HTTP flow, the release lookup is pointed at a bench-local server through the root-owned test seam, and the flow runs check → download (a pre-staged partial must RESUME, not restart) → verify — with the `/data`-floor and **bad-signature refusals proven against RAUC's real keyring**, refused bundles deleted — → install (in-flight flag armed, state says reboot-pending — **and the header badge carries reboot-pending plus the target version, so a reboot stays owed even with the modal closed** — nothing auto-reboots) → the explicit reboot intent → the new slot boots, the health gate commits, and the persisted **"updated" verdict reaches `/api/state`** | 15 + leg 4 |
308308
| `provision` | A config submitted through the wizard's real HTTP flow provisions the stack: validation, cosign-verified image pulls, containers running under podman, dashboard served through caddy. Before the successful submission, an unreachable remote node must be refused by preflight with its safe form values retained; a separate injected post-validation setup fault must return to the setup page with a useful error and the retained values for an ordinary corrected submit (#1955). The submitted config also enables the on-box miner, so the **built-in RigForge worker** must come up on its own, wired to the machine's own stratum, with mining held behind the sync gate cleanly. After provisioning, dashboard control applies a benign setting, refuses a disruptive setting without `APPLY`, accepts it with `APPLY`, returns doctor and log-tail reports, and creates a downloadable encrypted backup; the stack and dashboard must recover after that backup (#1931/#1965). Then a **reboot with no hands on it** must return the stack unaided through `pithead-boot`. M10 cuts that live stack three times and after EVERY cut requires every pre-cut container, image digest, non-regressing monerod height, miner, Caddy, and committed slot to survive. Finally the **commit gate's honesty** (#852) requires the real `pithead doctor --json` gate to PASS on the healthy still-syncing stack yet REFUSE once a revenue service is crashed. The provisioned boot also pins hugepages sizing, migration and floor-fallback behavior, and a Tari node that dies after the migrating slot commits: status, doctor and the dashboard must report it, and `./pithead up` must recover it. | Printed by the run |
309-
| `install` | The image boots as **removable** media (usb bus — the gate keys on it); the inventory offers the internal disk and never the boot medium; the real installer runs; the machine then boots from the target alone with a **complete** copy (`/var/lib/dpkg` — the overlay made an incomplete copy easy and invisible), a fresh machine-id, `/data` sized to the target, and the wizard serving. Then the **reinstall leg** plants a sentinel in `/data`, installs over the same disk, and requires the sentinel afterwards. The keep leg reinstalls from a **newer stick** over `/data` that holds the old dashboard image and digest record; the image ID and served page must change (#798). A 1.x `xmrig_proxy` pre-fill must migrate to `xvb` without carrying the removed key (#1954). The restore leg uploads the checked-in encrypted v1.20.0 fixture to an existing appliance disk, then proves the running stack carries its wallet, Tor identity, opaque RPC/onion secrets, and both fixture and target chain-data sentinels without a resync; its dashboard password survives while the bcrypt and fingerprint are regenerated and authenticate with it (#2001/#2230). | 34 |
309+
| `install` | The image boots as **removable** media (usb bus — the gate keys on it); the inventory offers the internal disk and never the boot medium; the real installer runs; the machine then boots from the target alone with a **complete** copy (`/var/lib/dpkg` — the overlay made an incomplete copy easy and invisible), a fresh machine-id, `/data` sized to the target, and the wizard serving. Then the **reinstall leg** plants a sentinel in `/data`, installs over the same disk, and requires the sentinel afterwards. The keep leg reinstalls from a **newer stick** over `/data` that holds the old dashboard image and digest record; the image ID and served page must change (#798). A 1.x `xmrig_proxy` pre-fill must migrate to `xvb` without carrying the removed key (#1954). The restore leg uploads the checked-in encrypted v1.20.0 fixture to an existing appliance disk, then proves the running stack carries its wallet, Tor identity, opaque RPC/onion secrets, and both fixture and target chain-data sentinels without a resync; its dashboard password survives with the archived bcrypt and fingerprint kept exactly, and authenticates with them (#2001/#2230/#2579). | 34 |
310310
| `rig` | The removable image installs the RigForge role, which mines from the baked binary with no stack containers and follows the A/B update contract. M13 cuts power while it mines, then requires a new boot, unattended mining, and the committed slot to return. | Printed by the run |
311311
| `media` | The physical-presence config stick shows the exact diff, applies after its countdown, is consumed, and cancels when removed mid-countdown. | Printed by the run |
312312
| `fault` | three power cuts mid-write; a deliberately corrupted bundle is refused without crashing and without bricking; a power cut inside the commit window; operator rollback after all of it; Fault D cuts an active first-boot baked-image load and requires the wizard and repaired image store afterwards; the box is still updatable afterwards | 15 |

‎docs/dev/appliance-wizard.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,8 @@ the same "validate before mutating real state" idiom `consume_preseed_config` al
246246
under a later release's new-configuration preflight policy.
247247
4. Regenerate `.env` and `Caddyfile` from the validated configuration, retaining only opaque
248248
generated secrets and Tor identity from the archived environment. The restored dashboard
249-
password remains in `config.json`; its bcrypt hash and fingerprint are regenerated from it.
249+
password remains in `config.json`; its archived bcrypt hash and fingerprint are kept exactly
250+
while the fingerprint matches it and the hash is well-formed, and are regenerated from it otherwise.
250251
Only on success: install the configuration files at mode `0600`, apply the accepted data
251252
trees, and publish `applied`. `data/tor` and `data/dashboard` (identity and the dashboard
252253
database) replace whatever is already there outright. `data/{monero,tari,p2pool}` — optional,
@@ -471,7 +472,7 @@ had a gap between it and the next one.
471472
| the artifact | `tests/os/verify-image.sh` | both role paths present in the shipped image: the boot script's fork, the unit conditions that admit each role, the baked prebuilt, no swap anywhere |
472473
| the real thing | `tests/os/run.sh --phase provision` | token from the console → submit → handoff → ack → running stack → built-in miner up and its shares accepted → reboot through a corrupted Caddyfile → no failed units → slot self-commit → miner back |
473474
| the other real thing | `tests/os/run.sh --phase rig` | the same page answered `RigForge` → rig card with no login → mining from the byte-identical baked binary → **no containers at all** → reboot owned by `pithead-boot`, wizard closed → slot self-commit on an unanswered pool → A/B install, uncommitted rollback, self-commit, persistence |
474-
| the restore leg | `tests/os/run.sh --phase install` | a checked-in encrypted v1.20.0 fixture generated from the signed compose bundle, uploaded through `/submit-restore` instead of the form onto an existing appliance disk — the running wallet, Tor identity and opaque RPC/onion secrets must match the prior-release fixture, the dashboard hash and fingerprint must be regenerated from the preserved password and authenticate with it, and both its chain sentinel and the target's pre-restore sentinel must survive |
475+
| the restore leg | `tests/os/run.sh --phase install` | a checked-in encrypted v1.20.0 fixture generated from the signed compose bundle, uploaded through `/submit-restore` instead of the form onto an existing appliance disk — the running wallet, Tor identity and opaque RPC/onion secrets must match the prior-release fixture, the dashboard hash and fingerprint follow the rule in step 4 of the restore flow (kept while the fingerprint matches the preserved password), and both its chain sentinel and the target's pre-restore sentinel must survive |
475476

476477
The orchestration row is the one that was missing. pytest proved the endpoint published the
477478
credentials; a render probe proved the card renders given them; nothing proved the app *asked*.

‎docs/dev/integration-testing.md‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@ Useful flags (full list in `run.sh --help`):
224224
| `--remote-monero-host <h>` | Bare host or IP for the external Monero node used by the `remote` scenario. Pair it with `--remote-monero-rpc-port` or `--remote-monero-zmq-port` when the node does not use ports 18081 and 18083. `e2e.sh` accepts the same flags and carries them through its read-only pregate and detached harness run. |
225225
| `--remote-tari-host <h>` | Bare host or IPv4 address for the external Tari node used by the `tari.mode=remote` scenario ([#103](https://github.com/p2pool-starter-stack/pithead/issues/103)). Pithead renders `tari.remote.grpc_port` separately; `e2e.sh` accepts and forwards the host. |
226226
| `--pruned-data-dir` / `--full-data-dir` | Synced alt DB to enable the opposite prune mode. |
227-
| `--lifecycle` | Also run the lifecycle phase (restart, apply secret-preservation, backup→restore, then uninstall→setup). Restore command, health, unreadable verification input, or failed restored pool/secrets assertion prevents later fault injection; peer-timing pool warnings remain non-fatal. |
227+
| `--lifecycle` | Also run the lifecycle phase (restart, apply secret-preservation, backup→restore, which must keep every wallet, proxy, dashboard, RPC, and onion secret category exact (the onion key reads need passwordless sudo), then uninstall→setup). Restore command, health, unreadable verification input, or failed restored pool/secrets assertion prevents later fault injection; peer-timing pool warnings remain non-fatal. |
228228
| `--fault-injection` | Also break monerod (stop / SIGSTOP / remove) and assert `status`' down/unhealthy/missing verdicts and the failover→recovery cycle, plus a dashboard DB-write fault (data dir made read-only → `/api/state` reports `db_healthy:false` → write access restored, [#202](https://github.com/p2pool-starter-stack/pithead/issues/202)). Destructive-then-restored; SSH or local; slow. The implementation uses the shared target wrapper, but a recorded SSH fault run is still tracked by [#2000](https://github.com/p2pool-starter-stack/pithead/issues/2000). |
229229
| `--image-upgrade <old-sha> <new-sha>` | Run the supported `pithead upgrade` path and prove old/new image identities, exact persistent mount sources, Monero/Tari captured-prefix anchors and non-regressing heights, durable dashboard table continuity, categorized secrets, returning workers, and resumed hashes. Prefix continuity does not claim that no same-chain bytes were re-downloaded. Requires exact lowercase 40-hex commits, `--candidate-bundle`, `--safety-backup`, and successful private reflink snapshots of every enumerated persistent mount while writers are stopped; no upgrade starts if any trust, backup, derived-state fingerprint, or snapshot check fails. |
230230
| `--candidate-bundle <tar.gz> <sig> <trusted-cosign.pub>` | Name the private candidate, detached signature, and externally anchored public key. All are absolute local paths; the signed archive's `PITHEAD_COMMIT` must equal `<new-sha>`. Before staging, the harness uses private snapshots to verify the bundle signature and key continuity, requires every Compose image to be digest-pinned, and verifies the five unique Pithead-built images' signatures and exact OCI revisions. Candidate-provided trust roots are rejected. |
@@ -566,6 +566,8 @@ and `--list` prints it).
566566
For one representative config:
567567

568568
- `restart` brings the stack back healthy (`status` → `0`), and backup → restore must do the same before a later fault-injection phase can run.
569+
The restore must also return every wallet, proxy, dashboard, RPC, and onion secret category
570+
exactly, the same per-category comparison a safety rollback makes.
569571
- An `apply` that changes the sidechain recreates only the affected containers and preserves
570572
secrets; the dashboard reflects the new pool; then it's reverted.
571573
- Node-down failover ([#31](https://github.com/p2pool-starter-stack/pithead/issues/31)):

0 commit comments

Comments
 (0)