Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ec7b27e
back-merge: main → develop after v1.10.2 (#723)
VijitSingh97 Jul 21, 2026
1058c0c
feat(#196): expose blocks/disk_growth/xvb_history on /api/state (Tier…
VijitSingh97 Jul 21, 2026
c5a3b1d
feat(#530): audit out-of-band config + rig changes, grouped by time (…
VijitSingh97 Jul 21, 2026
028e932
feat(#490): opt-in fail-closed miner hold on unrecoverable dashboard …
VijitSingh97 Jul 21, 2026
1b754db
feat(#249): warm the backup stack's XvB donation state on failover (#…
VijitSingh97 Jul 21, 2026
c98e200
feat(#593): opt-in co-located local miner hand-off (#725)
VijitSingh97 Jul 21, 2026
a7457a4
feat(#719): confirm-gate operationally-disruptive config fields, keep…
VijitSingh97 Jul 21, 2026
6687506
Merge develop (v1.10.2) into develop-v1.11 — reconcile #719 tests wit…
VijitSingh97 Jul 21, 2026
ca1d316
fix: reconcile #719 tests + docs with the #713 message change (dev co…
VijitSingh97 Jul 21, 2026
683e88b
build(deps): bump python from `baf8980` to `db3ff2e` in /build/dashbo…
dependabot[bot] Jul 21, 2026
b904454
build(deps): bump the actions group with 2 updates (#730)
dependabot[bot] Jul 21, 2026
4526ce2
build(deps): bump the python group in /build/dashboard with 3 updates…
dependabot[bot] Jul 21, 2026
8c5d1a5
fix(#724): cap per-worker rig-edit audit rows to bound the unauth-fee…
VijitSingh97 Jul 21, 2026
bb9cf55
feat(#728): allowlist dashboard-confirmed data-dir moves to the stack…
VijitSingh97 Jul 21, 2026
215157a
Merge remote-tracking branch 'origin/develop' into develop-v1.11
VijitSingh97 Jul 21, 2026
1b617fe
Merge pull request #732 from p2pool-starter-stack/develop-v1.11
VijitSingh97 Jul 21, 2026
df2b7a7
release: prep v1.11.0 — fleet resilience & hardening
VijitSingh97 Jul 21, 2026
6850580
Merge pull request #735 from p2pool-starter-stack/release-prep-v1.11.0
VijitSingh97 Jul 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ jobs:
env:
UV_PYTHON_DOWNLOADS: never # use the setup-python 3.11; don't fetch another interpreter
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0 # full history so diff-cover can diff the PR against origin/develop (#286)
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
- name: "Install uv (pinned — reproducible, hash-locked installs, #283)"
Expand All @@ -53,7 +53,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
Expand All @@ -70,7 +70,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Build the dashboard test stage (installs package + runs the suite in-container)
Expand All @@ -90,7 +90,7 @@ jobs:
matrix:
service: [monero, p2pool, tor, xmrig-proxy, dashboard]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: docker build ./build/${{ matrix.service }}
Expand All @@ -112,7 +112,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Install hadolint (pinned + sha256-verified, #286)"
Expand All @@ -131,10 +131,10 @@ jobs:
env:
UV_PYTHON_DOWNLOADS: never
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
- name: Install uv (pinned)
Expand All @@ -152,7 +152,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0 # full history so the scan covers every commit, not just the tip
Expand All @@ -171,10 +171,10 @@ jobs:
env:
UV_PYTHON_DOWNLOADS: never
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
- name: Install uv (pinned)
Expand All @@ -190,7 +190,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: "Install shellcheck + shfmt (pinned + sha256-verified, #286)"
Expand Down Expand Up @@ -227,7 +227,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Validate docker-compose.yml interpolation + hardening invariants (#90)
Expand All @@ -240,13 +240,13 @@ jobs:
env:
UV_PYTHON_DOWNLOADS: never
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: "20"
- uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.11"
- name: Install uv (pinned)
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/integration-mini-stack.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# ubuntu-latest ships Docker with the Compose v2 plugin — no setup needed.
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/lychee.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check links in Markdown
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
# box; prefer an ephemeral / just-in-time runner in its own runner group.
runs-on: [self-hosted, pithead-release]
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

Expand Down
89 changes: 89 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,95 @@ Pithead ships as **one product, one version** — the version lives in the top-l
[`VERSION`](VERSION) file and every released image is tagged with it. Releases are cut
per the process in [`docs/dev/releasing.md`](docs/dev/releasing.md).

## [Unreleased]

## [1.11.0] - 2026-07-21

### Added

- **Confirm-gated config editing for operationally-disruptive settings** (#719). The dashboard
config editor can now commit a small set of disruptive-but-recoverable settings behind a
type-to-confirm, instead of refusing them outright: the four service data directories (a move
re-syncs), the stratum port (rigs must repoint), the Monero/Tari clearnet initial-sync toggles
(the host IP is exposed during IBD, then auto-reverts to Tor), and enabling Monero pruning. Each
renders editable with a "you'll type `APPLY` to confirm" affordance; the typed confirmation rides
to the host approval gate, which requires it before the change proceeds and records the apply in
the control audit log as a distinct `commit-confirmed` action.

### Changed

- **The control-channel security perimeter stays host-only** (#719). Type-to-confirm is UX
friction, not a security control — a compromised dashboard that can set a field can also fill the
confirm box — so the perimeter is unchanged: wallets and view keys, the dashboard login and onion
settings, the control channel itself, the Tor egress firewall, the stratum password, node
endpoints and credentials, and the per-rig hosts and tokens all remain refused from the dashboard,
as does the heavier direction of a confirm-gated key (disabling pruning forces a full re-sync).
- **Dashboard-confirmed data-directory moves are allowlisted to the stack's data root** (#728).
#719 made the four `*_DATA_DIR` moves confirmable from the dashboard behind a typed `APPLY`, but
the destination was still checked only by the host-side blocklist (`assert_safe_dir`), which
passes any non-catastrophic absolute path. A confirmed move from the dashboard is now further
held to an allowlist: the new location must sit under the stack's own data root (the install
dir's `data/`) or a parent the stack already keeps data in, else the move is refused even with
the typed confirmation and stays host-CLI only. The host `./pithead apply` path keeps the wider
blocklist — a shell operator already has filesystem-wide reach; only the dashboard-reachable move
is tightened, closing the destination trust-escalation the confirm-gate opened.
- **Opt-in local miner** (#593). A box that runs the stack 24/7 can mine with its spare CPU by
co-locating a RigForge worker on the stack host. `./pithead setup` now asks "Also mine on this
machine with its spare CPU?" (off by default; also the new `local_miner.enabled` config flag),
and setup/apply print the two values a RigForge install needs — the stack's own stratum URL
(loopback `127.0.0.1:3333`, or the configured `p2pool.stratum_bind`/`stratum_port`) and the
stratum secret already in `.env`. The co-located worker self-registers through the proxy like any
other rig. Pithead only declares the intent and hands off those values; RigForge owns all
host-level tuning (HugePages, GRUB, MSR, governor) and the miner service. See
[docs/workers.md](docs/workers.md#mine-on-the-stack-host-itself).
- **Warm XvB donation state on a backup stack (#249).** On a two-host failover pair — same wallet,
workers listing both hosts in `pools[]` — the backup's XvB donation controller used to cold-start
when the fleet failed over to it: the closed-loop split restarted from the feedforward estimate
and re-ramped for hours, over- or under-shooting the credited tier until it reconverged. The
controller's commanded donation fraction is now persisted, so a plain restart resumes the warmed
split instead of re-seeding cold. A backup can also point `xvb.standby.source` at the primary
dashboard's new read-only `/api/xvb-standby` endpoint; it periodically pulls the primary's
controller state and holds it as standby (inspectable in `/api/state`), then adopts it the first
time it actually donates at failover — so the split resumes warm. One-way (backup pulls from
primary), inert unless configured, and never acted on while the primary is authoritative (an idle
backup has no workers, so its controller stays on P2Pool). The pull follows the dashboard's
privacy-safe egress rule: an `.onion` source, a public IP, or any hostname rides the bridge Tor
SOCKS (the primary sees a Tor exit, never the backup's IP); only a provably-private/loopback IP
literal dials direct as a LAN hop — so the pull never opens a clearnet path, and the Security
panel reports its route.
- **Opt-in fail-closed miner hold on an unrecoverable dashboard health failure** (#490). New
`dashboard.fail_closed` toggle, default **off**. The dashboard is an observability layer, not the
mining datapath (`xmrig-proxy` → `p2pool` → `monerod` runs independently of it), so by default an
unhealthy condition only alerts (Telegram/Healthchecks/webhook) and shows a badge while mining
continues. Set it `true` and a genuinely unrecoverable failure — the SQLite database failing to
rebuild after its own auto-heal attempt (disk full, permissions), or the `dashboard` container
itself crash-looping past the #337 debounce — holds `p2pool` and `xmrig-proxy` using the same
#35 sync-gate stop/start mechanism, with a `Miner held (fail-closed)` badge. Unlike the sync
gate's one-way latch it re-checks every cycle and releases on its own once the condition clears.
A transient write blip, a slow query, or a single failed external fetch never trips it — those
still only alert. Gated by the #33 control-approval path like other `dashboard.*` toggles.
- **`/api/state` exposure for three of the #196 telemetry-backbone series (Tier-1).** The backbone
PR (#600) added five persisted SQLite tables with capture, storage, and retention, but shipped
without surfacing them to the client. This slice exposes three — `blocks` (pool block-found
events), `disk_growth` (hourly monerod-DB-size + host-disk-usage samples), and `xvb_history`
(~5-min XvB-credited scalar samples) — as range-filtered arrays under those same keys, bounded
at the existing 700-point chart cap for the two higher-cadence series. `network_history` and
`worker_history` are a separate (Tier-2) slice, not touched here. No chart renders any of these
series yet — that's a further follow-up.

### Security

- **The out-of-band audit trail is now flood-capped per worker (#724).** The `rig-edit` audit
source (#530) reads a worker's reported change id off the unauthenticated LAN worker feed. The
#530 deterministic row id collapses *repeats* of one change id to a single row, but not *distinct*
ones — so a malicious or compromised device presenting as a worker could report a fresh random
change id every poll, writing a new permanent `audit_events` row each ~30s cycle and slowly
filling the dashboard database (the table has no pruning). New `rig-edit` rows are now capped per
worker per rolling hour; beyond the cap the extra rows are dropped behind a single `rate-limited`
marker row and a logged warning, so the flood stays visible instead of growing the table without
limit. A genuine occasional rig change still records normally, and the non-attacker-controllable
`host-edit` and mirrored `control.log` rows are unaffected.

## [1.10.2] - 2026-07-21

### Fixed
Expand Down
35 changes: 27 additions & 8 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,21 @@ The stack's defaults:
API (over the stack's Tor SOCKS), refuses any mismatch or non-release tag, and limits attempts
to one per 10 minutes — the container cannot choose an image, tag, or registry. Enabling the
channel without a dashboard password is a validation error, on a published onion it additionally
requires Tor client authorization, and every mutation is audited host-side. Commits are default-denied against an explicit allowlist of
operational settings: a commit that changes any env key off that list — in every direction
(enabling, changing, or disabling) — is refused, as is anything the change preview flags
destructive. Wallets, dashboard auth and onion exposure, the control channel itself, the Tor
egress firewall, clearnet toggles, node endpoints, binds, and every credential are off the
list, and a key added in the future stays un-committable until deliberately listed. Those
edits must be applied from the host CLI; out-of-band approval is tracked in
[#338](https://github.com/p2pool-starter-stack/pithead/issues/338).
requires Tor client authorization, and every mutation is audited host-side. Commits are default-denied against an explicit allowlist. Low-risk
operational settings commit directly; a small set of operationally-disruptive ones — data-directory
moves, the stratum port, enabling clearnet initial sync, and enabling pruning — commit only behind
a typed confirmation in the dashboard, and only in that direction. A dashboard-confirmed
data-directory move is further held to an **allowlist** (#728): the new location must sit under the
stack's own data root (the install dir's `data/`) or a parent the stack already keeps data in;
a move to any other absolute path is refused even with the typed confirmation and stays host-CLI
only. The host CLI keeps its wider blocklist check — a shell operator already has filesystem-wide
reach. Everything else is refused in
every direction, as is anything the change preview flags destructive (including the heavy direction
of a confirm-gated key, e.g. disabling pruning, which forces a full re-sync). The security
perimeter — wallets and view keys, dashboard auth and onion exposure, the control channel itself,
the Tor egress firewall, node endpoints, binds, every credential, and the per-rig hosts and tokens —
is never dashboard-committable, with or without the typed confirmation. A key added in the
future stays un-committable until deliberately listed. Those edits must be applied from the host CLI.
- Attack visibility (#349): Caddy writes a JSON access log for every dashboard vhost (LAN and
onion), and the control channel's host-side audit log records who changed what (setting names
only, never values). The dashboard surfaces both read-only — a burst of 401s is the
Expand All @@ -82,6 +89,18 @@ The stack's defaults:
anonymous prober stored XSS against the operator. Both logs are size-bounded (Caddy's native
rolling; a trim-before-append cap in the audit writer). Neither ever records a secret: Caddy
redacts credential headers by default, and the audit writer logs key names only.
- Out-of-band change detection (#530): the audit trail above only sees requests the dashboard
itself handled. Its poll loop separately watches for a `config.json` change with no matching
control-channel commit, and a worker control-API report for a change the dashboard never sent,
and appends both — `host-edit` / `rig-edit` — to the same trail, keys or worker names only. The
persisted trail (mirrored `control.log` rows plus these two out-of-band kinds) lives in the
dashboard's own database, not just the log tail, so the Security panel's hour/day/month grouping
covers more than `control.log`'s own trimmed window. The `rig-edit` source reads off the
unauthenticated worker feed, so it is rate-capped per worker (#724): a rig reporting distinct
change_ids on every poll can add at most a bounded number of rows per hour before the rest are
dropped behind a single `rate-limited` marker — one LAN device can't grow the database without
limit. The `host-edit` and mirrored `control.log` rows are not attacker-controllable and are not
capped.

### Telegram control commands (#338)

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.10.2
1.11.0
2 changes: 1 addition & 1 deletion build/dashboard/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# uv's own CVEs into image scans, #282). uv lives only in the build/test stages.
# ==========================================================================
# Pinned by digest (#135) so the python:3.11-slim tag can't be silently re-pointed.
FROM python:3.11-slim@sha256:baf89808ec37adeaab83cec287adb4a2afa4a11c1d51e961c7ec737877e61af6 AS base
FROM python:3.11-slim@sha256:db3ff2e1800a8581e2c48a27c3995339d47bdf046da21c7627accd3d51053a93 AS base

# Run from a project venv on PATH (so entrypoint.sh's `python3` resolves to it); use the
# digest-pinned base interpreter (never let uv download a different Python); compile bytecode and
Expand Down
Loading