You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(control): pin the D10 reachability guard to origin/main (#318) (#319)
The guard checked the target tag against origin/HEAD (falling back to
origin/main only when unset). The repo's default branch is develop, and
since the develop->main promotion moved to merge commits, release tags
point at main-only commits — so a freshly cloned rig (origin/HEAD ->
develop) refused every legitimate remote upgrade with a rollback. The
deployed fleet was unaffected only because its clones happen to point
origin/HEAD at main (verified on miner-0 during the #308 real e2e).
Releases are cut from main, so main is the expected branch: pin
merge-base --is-ancestor to origin/main explicitly and drop the
symbolic-ref lookup. Fail-closed corollary: a clone without origin/main
(e.g. single-branch) refuses. ADR 0002 D10 amended in place;
config.reference.json wording updated; the unit-test git stub's dead
symbolic-ref arm removed and two drift guards added (guard pins
origin/main; no symbolic-ref anywhere in the script).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: config.reference.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -1,5 +1,5 @@
1
1
{
2
-
"_docs": "Reference for every config.json key, each shown with its default value. Copy ONLY the keys you actually want to change into config.json; any key you omit keeps the default shown here. The pool target is the native XMRig 'pools' array: each pool needs a 'url' (host:port); every other field falls back to a Pithead-friendly default. A pool's 'user' is the rig's label on the dashboard (defaults to the machine hostname); list multiple entries for failover. An empty ACCESS_TOKEN (the default) leaves the rig's read-only HTTP API open (no token) — which matches Pithead's default no-auth stats probe; set a value only to require a Bearer token (then match it dashboard-side). 'autotune' is one of \"disabled\" (default — no scheduled tuning), \"performance\" (monthly tune for raw hashrate), or \"efficiency\" (monthly tune for hashrate-per-watt). 'watchdog' set to \"enabled\" installs a timer that health-checks the miner every 'watchdog_interval_min' minutes (default 5) and restarts it after two consecutive checks see 0 H/s or a dead API; 'max_temp_c' (empty = off) additionally stops the miner above that °C and starts it again 5°C below — check what your board's thermal_zone0 reports before setting it. 'miner_user' (empty = run as root, the default) runs the miner as that dedicated non-root system user — RigForge applies the MSR preset root-side; on CPU families without a known preset the ~10-15% MSR boost is skipped. 'api_allow_from' (empty = off) scopes the API port(s) — including the writable control port when 'control' is enabled — to one IPv4 or IPv6 address/CIDR source + loopback via an own nftables table (Linux, needs nft; for an IPv6 source the served port must bind a v6 address, so set 'api_bind'/'control_bind' to \"::\"). Set 'api' to enabled for the sister API: a second read-only port (default 8081) serving XMRig's summary enriched with RigForge tune/power/health/provenance data, gated by the same ACCESS_TOKEN; 'api_port'/'api_bind' place it. Set 'control' to \"enabled\" for the WRITABLE control path (#236): a SEPARATE authenticated port (default 8082, 'control_port'/'control_bind' place it) that lets a Pithead stack apply config changes through RigForge so config.json stays authoritative. Fail-closed: enabling it REQUIRES both ACCESS_TOKEN and api_allow_from (a writable API with no token or no pinned source is refused). Only pools, DONATION, autotune, watchdog(+interval), and max_temp_c are writable through it (and the REMOTE path refuses to disable watchdog or to unset/out-of-band max_temp_c — a rig's thermal protection can only be removed by a local rigforge.sh apply, #257); every change is validated, the old config is snapshotted to config-backups/, and a change that doesn't come back live is rolled back. Set 'control_upgrade' to \"enabled\" (a SECOND opt-in, only valid when 'control' is also enabled) to let the stack trigger this rig to upgrade its own RigForge to the latest release (#308, ADR 0002) — i.e. fetch and run new root code on a remote trigger. Default off, and never on just because 'control' is: the rig refuses any target that isn't a real release newer than the one running, verifies the release commit is reachable on the default branch, throttles repeat runs, and health-gates the swap with rollback. It rides the control path's own Bearer token + api_allow_from pin. Releases are checksummed, not signed — GitHub is the trust root (see SECURITY.md). For co-locating a miner on a busy box (e.g. a Pithead stack host), 'hugepages_reserve_extra_mb' (default 0) adds that many MB of HugePages to RigForge's computed mining reservation so the pool covers stack + miner — RigForge stays the sole writer of the reservation and, when the box's existing reservation already covers both, changes nothing and needs no reboot; 'threads' (empty/null = auto) caps the RandomX thread count (a ceiling — min(auto, threads), e.g. nproc-2) so the miner leaves spare cores, and it sizes the reservation to match. A TLS pool can pin the server certificate with 'tls-fingerprint' (its SHA-256 as 64 hex chars; null = no pin) — XMRig does no CA verification for stratum, so the pin is the only server authentication TLS stratum has. Full descriptions: https://github.com/p2pool-starter-stack/rigforge/blob/main/docs/configuration.md#configuration-reference",
2
+
"_docs": "Reference for every config.json key, each shown with its default value. Copy ONLY the keys you actually want to change into config.json; any key you omit keeps the default shown here. The pool target is the native XMRig 'pools' array: each pool needs a 'url' (host:port); every other field falls back to a Pithead-friendly default. A pool's 'user' is the rig's label on the dashboard (defaults to the machine hostname); list multiple entries for failover. An empty ACCESS_TOKEN (the default) leaves the rig's read-only HTTP API open (no token) — which matches Pithead's default no-auth stats probe; set a value only to require a Bearer token (then match it dashboard-side). 'autotune' is one of \"disabled\" (default — no scheduled tuning), \"performance\" (monthly tune for raw hashrate), or \"efficiency\" (monthly tune for hashrate-per-watt). 'watchdog' set to \"enabled\" installs a timer that health-checks the miner every 'watchdog_interval_min' minutes (default 5) and restarts it after two consecutive checks see 0 H/s or a dead API; 'max_temp_c' (empty = off) additionally stops the miner above that °C and starts it again 5°C below — check what your board's thermal_zone0 reports before setting it. 'miner_user' (empty = run as root, the default) runs the miner as that dedicated non-root system user — RigForge applies the MSR preset root-side; on CPU families without a known preset the ~10-15% MSR boost is skipped. 'api_allow_from' (empty = off) scopes the API port(s) — including the writable control port when 'control' is enabled — to one IPv4 or IPv6 address/CIDR source + loopback via an own nftables table (Linux, needs nft; for an IPv6 source the served port must bind a v6 address, so set 'api_bind'/'control_bind' to \"::\"). Set 'api' to enabled for the sister API: a second read-only port (default 8081) serving XMRig's summary enriched with RigForge tune/power/health/provenance data, gated by the same ACCESS_TOKEN; 'api_port'/'api_bind' place it. Set 'control' to \"enabled\" for the WRITABLE control path (#236): a SEPARATE authenticated port (default 8082, 'control_port'/'control_bind' place it) that lets a Pithead stack apply config changes through RigForge so config.json stays authoritative. Fail-closed: enabling it REQUIRES both ACCESS_TOKEN and api_allow_from (a writable API with no token or no pinned source is refused). Only pools, DONATION, autotune, watchdog(+interval), and max_temp_c are writable through it (and the REMOTE path refuses to disable watchdog or to unset/out-of-band max_temp_c — a rig's thermal protection can only be removed by a local rigforge.sh apply, #257); every change is validated, the old config is snapshotted to config-backups/, and a change that doesn't come back live is rolled back. Set 'control_upgrade' to \"enabled\" (a SECOND opt-in, only valid when 'control' is also enabled) to let the stack trigger this rig to upgrade its own RigForge to the latest release (#308, ADR 0002) — i.e. fetch and run new root code on a remote trigger. Default off, and never on just because 'control' is: the rig refuses any target that isn't a real release newer than the one running, verifies the release commit is reachable on main (the release branch), throttles repeat runs, and health-gates the swap with rollback. It rides the control path's own Bearer token + api_allow_from pin. Releases are checksummed, not signed — GitHub is the trust root (see SECURITY.md). For co-locating a miner on a busy box (e.g. a Pithead stack host), 'hugepages_reserve_extra_mb' (default 0) adds that many MB of HugePages to RigForge's computed mining reservation so the pool covers stack + miner — RigForge stays the sole writer of the reservation and, when the box's existing reservation already covers both, changes nothing and needs no reboot; 'threads' (empty/null = auto) caps the RandomX thread count (a ceiling — min(auto, threads), e.g. nproc-2) so the miner leaves spare cores, and it sizes the reservation to match. A TLS pool can pin the server certificate with 'tls-fingerprint' (its SHA-256 as 64 hex chars; null = no pin) — XMRig does no CA verification for stratum, so the pin is the only server authentication TLS stratum has. Full descriptions: https://github.com/p2pool-starter-stack/rigforge/blob/main/docs/configuration.md#configuration-reference",
Copy file name to clipboardExpand all lines: docs/adr/0002-remote-worker-upgrade.md
+3-3Lines changed: 3 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -40,7 +40,7 @@ As in ADR 0001 D2, the network-facing receiver stays unprivileged (`DynamicUser`
40
40
The staged body is tiny: `{"version": "vX.Y.Z"}`, and it **is** the target. The rig does **not** make its own `api.github.com/releases/latest` call: a per-rig version-check dial re-introduces the "version beacon" the project's no-phone-home posture forbids (it leaks each rig's public IP over clearnet, correlatable over time), and behind one NAT the fleet would share GitHub's 60/hr unauthenticated bucket, so a fan-out or a looping trigger could `403` every rig at once. pithead #597 already re-derives the real latest host-side over Tor; the rig trusts that supplied tag but **bounds what it will act on**, so a compromised trigger can only ever pick a *real, published, reachable release newer than what is installed* — never an arbitrary tag, a downgrade, or a dangling commit:
41
41
42
42
-**Monotonic anti-rollback** — refuse a target version ≤ installed (D10).
43
-
-**Reachable-from-default-branch** — refuse a commit that is not an ancestor of `origin/<default>` (D10).
43
+
-**Reachable-from-main** — refuse a commit that is not an ancestor of `origin/main` (D10; amended by #318).
44
44
-**Immutable releases + tag protection** (D10) make the tag→commit binding a platform guarantee.
45
45
46
46
The rig still `git`-fetches the tag directly from GitHub — the code has to come from somewhere, and that fetch happens only on an actual upgrade (rare, throttled), not on every trigger — but it makes no separate version-check dial. Anything that fails a guard is terminal `failed`. This trades away rig-*independent* confirmation of "is this THE latest" (the dashboard, over Tor, is the deriver) for privacy, fewer moving parts, and no fleet rate-limit coupling; the anti-rollback + reachability guards are what make trusting a supplied target safe.
@@ -82,7 +82,7 @@ It comes from the **verb**, which treats the staged file as untrusted input: `sy
82
82
Low-cost integrity controls that need no standing signing key, shoring up the GitHub-as-trust-root bet in D5:
83
83
84
84
-**Enable GitHub Immutable Releases** (GA 2025-10) + a tag-protection ruleset on the release-tag pattern. This locks a release's tag to a specific commit, blocks tag moves, and blocks repo-resurrection (reusing a tag after delete/recreate — directly relevant, since `main` was deleted and recreated on 2026-07-17). It turns "the tag could be silently moved" from an accepted risk into a platform guarantee. **Operational prerequisite for recommending the feature on.**
85
-
-**Reachable-from-default-branch check:** refuse a target whose commit is not an ancestor of `origin/<default>` (`git merge-base --is-ancestor`), per SLSA's "source revision reachable from an expected branch." Kills the tag-points-at-a-dangling-commit class.
85
+
-**Reachable-from-main check:** refuse a target whose commit is not an ancestor of `origin/main` (`git merge-base --is-ancestor`), per SLSA's "source revision reachable from an expected branch." Kills the tag-points-at-a-dangling-commit class.*Amended 2026-07-18 (#318): originally "reachable from `origin/<default>`" via `origin/HEAD`. But the repo's default branch is `develop`, and since the develop→main promotion moved to merge commits, release tags point at main-only commits — so a freshly cloned rig (whose `origin/HEAD` resolves to develop) refused every legitimate release. Releases are cut from main; main is the expected branch, so the guard pins `origin/main` explicitly. Fail-closed corollary: a clone without `origin/main` (e.g. single-branch) also refuses.*
86
86
-**Monotonic anti-rollback:** refuse a target version ≤ the installed version (TUF's rollback defense). This is the primary guard against a compromised trigger naming an older, known-vulnerable release, and it is what makes accepting a dashboard-supplied target safe (see D4).
87
87
-**Roadmap, not now:** keyless cosign via GitHub Actions OIDC is near-free to adopt later (no long-lived key) and would move the fleet from "trust GitHub" to "trust GitHub + a public transparency log." Deferred, not a blocker.
88
88
@@ -100,7 +100,7 @@ Low-cost integrity controls that need no standing signing key, shoring up the Gi
100
100
- New opt-in surface, off by default, fail-closed. A rig that does not enable `control_upgrade` carries zero new capability; a rig on `control` alone is unchanged.
101
101
- Residual risk accepted: GitHub-account/release compromise → fleet-wide root RCE (D5). Mitigation is account hardening, not signing.
102
102
- Producer for pithead #597. Backwards compatible (a new MINOR capability). It must ship in a RigForge release before pithead #597 can run end-to-end; pithead #596 (version badge) has no RigForge dependency and ships first.
103
-
- Consumer contract: `POST :8082/upgrade {"version":"vX.Y.Z"}` → `202 Accepted` + change id → poll `:8082/status` for `applied` / `rolled_back` / `failed`. The host never trusts a host/port/token from the intent; the rig bounds the dashboard-supplied target with monotonic anti-rollback + reachable-from-default-branch guards (D4/D10) rather than making its own version-check dial.
103
+
- Consumer contract: `POST :8082/upgrade {"version":"vX.Y.Z"}` → `202 Accepted` + change id → poll `:8082/status` for `applied` / `rolled_back` / `failed`. The host never trusts a host/port/token from the intent; the rig bounds the dashboard-supplied target with monotonic anti-rollback + reachable-from-main guards (D4/D10) rather than making its own version-check dial.
104
104
105
105
## Open questions (resolved 2026-07-17 — see Resolution above)
0 commit comments