Auto-Update
Auto-update shipped across Phases 8c–8g: update-check + verified download as CLI subcommands (8c Foundation), then atomic swap + health check + rollback wired into the daemon itself (8d Wiring onward). All of it is in the current release; the sections below cover the CLI entry points and the daemon flow.
CLI update commands (Phase 8c)
Two CLI commands, manually triggered by the operator:
dvb-warppool-cli check-update
Fetches the latest release from the project forge (Forgejo) and compares it against the
installed version (CARGO_PKG_VERSION from the CLI binary).
$ dvb-warppool-cli check-update
Installed: 1.4.0
Latest: v1.4.1 (Release v1.4.1)
Update: 1.4.0 → 1.4.1 (available)
Release-Assets:
· dvb-warppool-v1.4.1-x86_64-unknown-linux-gnu.tar.gz (15728640 bytes)
· dvb-warppool-v1.4.1-aarch64-unknown-linux-gnu.tar.gz (14680064 bytes)
· dvb-warppool_1.4.1-1_amd64.deb (9437184 bytes)
· SHA256SUMS (1280 bytes)
· SHA256SUMS.bundle (2048 bytes)
Optional --json for scripting. --repo owner/name for forks or tests.
dvb-warppool-cli download-update
Downloads a release asset with sha256 verification. The Linux/macOS
assets are tarballs (dvb-warppool-<tag>-<triple>.tar.gz); the CLI
verifies the tarball’s sha256 and then extracts the dvb-warppool-daemon
binary to --to. It performs no atomic swap and no restart —
that’s the operator’s job, so the workflow stays auditable.
# Host-matching asset, auto-detected via the Rust target-triple
# (x86_64-unknown-linux-gnu / aarch64-unknown-linux-gnu / *-apple-darwin)
$ dvb-warppool-cli download-update \
--to /tmp/dvb-warppool-daemon \
--sha256 abcd1234... # tarball-sha aus SHA256SUMS
Download: dvb-warppool-v1.4.1-x86_64-unknown-linux-gnu.tar.gz (15728640 bytes)
✓ 15728640 bytes geladen, sha256=abcd1234...
✓ dvb-warppool-daemon (13007200 bytes) aus dem Tarball extrahiert → /tmp/dvb-warppool-daemon
→ Installieren (Operator-Schritt — kein Auto-Swap):
sudo install -m 755 /tmp/dvb-warppool-daemon /usr/local/bin/dvb-warppool-daemon
sudo systemctl restart dvb-warppool
The sha256 you pass is the tarball’s hash — the SHA256SUMS lines
cover the published assets (the .tar.gz), not the binary inside. On
mismatch the downloaded tarball is deleted — no “tainted download” left
lying around. --asset <substring> overrides the host auto-detection. --tag vX.Y.Z
downloads a specific release instead of latest — to pin a known-good
version or downgrade off a bad one (pass the tag with its v prefix; it hits
releases/tags/<tag> instead of releases/latest).
Recommended operator flow
#!/bin/bash
set -euo pipefail
TAG=$(dvb-warppool-cli check-update --json | jq -r .latest)
[ "$TAG" = "null" ] && exit 0 # no update
# fetch SHA256SUMS from the release and verify with cosign
curl -sSfLO https://git.warppool.org/dvb-projekt/dvb-WarpPool/releases/download/$TAG/SHA256SUMS
curl -sSfLO https://git.warppool.org/dvb-projekt/dvb-WarpPool/releases/download/$TAG/SHA256SUMS.bundle
curl -sSfLO https://git.warppool.org/dvb-projekt/dvb-WarpPool/raw/branch/main/cosign.pub
# Project-key signing since the forge migration: the signature ships as a
# self-contained cosign 3.x *bundle*, so verify with --bundle, not --signature.
# GitHub-era releases (<= v1.15.0) were keyless-signed with a detached
# SHA256SUMS.sig — those assets are gone with the account.
cosign verify-blob \
--key cosign.pub \
--bundle SHA256SUMS.bundle SHA256SUMS
# extract the expected sha256 for the host tarball
TRIPLE="x86_64-unknown-linux-gnu" # or aarch64-unknown-linux-gnu / *-apple-darwin
ASSET="dvb-warppool-${TAG}-${TRIPLE}.tar.gz"
EXPECTED=$(awk -v a="$ASSET" '$2 == a {print $1}' SHA256SUMS)
# download + verify the tarball, then extract the daemon binary to --to
dvb-warppool-cli download-update --asset "$TRIPLE" \
--to /tmp/dvb-warppool-daemon --sha256 "$EXPECTED"
# manual swap (the daemon's POST /api/admin/update automates this)
sudo install -m 755 /tmp/dvb-warppool-daemon /usr/local/bin/dvb-warppool-daemon
sudo systemctl restart dvb-warppool
Crate internals (warppool-autoupdate)
#![allow(unused)]
fn main() {
use warppool_autoupdate::UpdateChecker;
let checker = UpdateChecker::new("dvb-projekt/dvb-WarpPool");
let release = checker.fetch_latest().await?;
if let Some(newer) = checker.is_newer_than(&release, env!("CARGO_PKG_VERSION"))? {
// update available
}
}
Modules:
version— semver-subset parser (major.minor.patch + optional-pre), compared viaOrd. Pre-release < stable (semver convention).release— forge release API (GitHub-compatible JSON schema) with minimalserdedeserialization (no octocrab — that would be 30+ transitive deps for 3 fields).download—download_verified(client, url, dest, expected_sha)with streaming write + SHA-256 accumulator. Mismatch → file deleted.swap—atomic_swap(new, current, backup_to)with POSIX rename (atomic on the same filesystem). Setschmod 0755on the new binary, optionally backs up the old one tobackup_to.
Phase 8d — API endpoints
Since Phase 8d the daemon exposes two admin-protected endpoints for
GUI-driven auto-update (activated via env-var WARPPOOL_AUTOUPDATE_REPO=owner/name,
otherwise 503).
GET /api/admin/update-check
Returns latest-release metadata + newer flag in a single request.
curl -H "Authorization: Bearer $TOKEN" \
http://localhost:18334/api/admin/update-check
{
"current": "1.4.0",
"latest": "v1.4.1",
"name": "Release v1.4.1",
"prerelease": false,
"newer": "1.4.1",
"assets": [
{"name": "dvb-warppool-v1.4.1-x86_64-unknown-linux-gnu.tar.gz", "size": 15728640, "url": "..."},
{"name": "SHA256SUMS", "size": 1280, "url": "..."}
]
}
Audit: update.check with the resolved latest tag as target.
POST /api/admin/update
Downloads the chosen asset, verifies sha256, and — for the Linux/macOS
.tar.gz assets — extracts the dvb-warppool-daemon binary before the
atomic rename into target_path. asset is an optional substring match;
omit it and the daemon picks the asset matching its own host
target-triple. Native installers (.msi/.dmg/.deb/.rpm/.AppImage)
are rejected here — use the platform installer. Does not send a
restart — the operator does that via systemctl restart from the
restart_hint field.
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"asset": "x86_64-unknown-linux-gnu",
"sha256": "abcd1234...",
"target_path": "/usr/local/bin/dvb-warppool-daemon",
"backup_path": "/var/lib/dvb-warppool/backup/daemon.v1.4.0"
}' \
http://localhost:18334/api/admin/update
{
"tag": "v1.4.1",
"asset": "dvb-warppool-v1.4.1-x86_64-unknown-linux-gnu.tar.gz",
"bytes": 15728640,
"sha256": "abcd1234...",
"target_path": "/usr/local/bin/dvb-warppool-daemon",
"backup_path": "/var/lib/dvb-warppool/backup/daemon.v1.4.0",
"installed_from_archive": true,
"restart_hint": "systemctl restart dvb-warppool"
}
Audit: update.applied (ok=true) or update.failed (ok=false with
details). The operator can review the update history via
/api/admin/audit.
Activation
In dvb-warppool.service (systemd):
[Service]
Environment="WARPPOOL_AUTOUPDATE_REPO=dvb-projekt/dvb-WarpPool"
Or directly:
WARPPOOL_AUTOUPDATE_REPO=dvb-projekt/dvb-WarpPool dvb-warppool-daemon
Phase 8e — Periodic auto-check + SSE event
When auto-update is enabled (WARPPOOL_AUTOUPDATE_REPO=…), the daemon
spawns a background task that runs fetch_latest every N hours. On a
newer release an update_available SSE event is pushed to the event
bus — the UI subscribes to it and can show an “Update available” banner
without the operator having to click update-check manually.
Activation
# Standard: 24h interval
WARPPOOL_AUTOUPDATE_REPO=dvb-projekt/dvb-WarpPool dvb-warppool-daemon
# Custom interval
WARPPOOL_AUTOUPDATE_REPO=dvb-projekt/dvb-WarpPool \
WARPPOOL_AUTOUPDATE_INTERVAL_HOURS=6 \
dvb-warppool-daemon
# Periodic disabled, on-demand only via /api/admin/update-check
WARPPOOL_AUTOUPDATE_REPO=dvb-projekt/dvb-WarpPool \
WARPPOOL_AUTOUPDATE_INTERVAL_HOURS=0 \
dvb-warppool-daemon
Initial delay is 60s after daemon start so that not every pool instance hits the forge at the same time.
SSE event schema
Subscribe via Browser EventSource at /api/events. The event looks like:
event: update_available
data: {"type":"update_available","at":"2026-05-26T12:00:00Z","current":"0.1.0","latest":"v0.2.0","name":"Release 0.2.0","prerelease":false}
JavaScript example for a UI banner:
const es = new EventSource("/api/events");
es.addEventListener("update_available", (ev) => {
const data = JSON.parse(ev.data);
showBanner(`Update available: ${data.current} → ${data.latest}`);
});
On errors (rate-limit, network-down)
The task logs warn! and retries on the next interval. With the default 24 h
interval a pool makes fewer than 2 release-API requests a day, so no rate limit
on the forge side is anywhere near being a concern.
Phase 8f — Systemd OnFailure rollback
So that a bad auto-update doesn’t leave the daemon in a permanently-down
state, the shipped dvb-warppool.service has an OnFailure= hook that
on repeated crashes (StartLimitBurst=4 times within
StartLimitInterval=300s) restores the most recent backup and restarts.
Layout
/usr/lib/systemd/system/dvb-warppool.service ← main service
/usr/lib/systemd/system/dvb-warppool-rollback.service ← oneshot helper
/usr/lib/dvb-warppool/rollback.sh ← restore script
Flow
- Daemon starts with the new binary.
- Crash within 5s → systemd
Restart=on-failureretries. - After 4 crashes in 5 minutes → systemd gives up and triggers
OnFailure=dvb-warppool-rollback.service. rollback.sh:- Finds
/var/lib/dvb-warppool/backup/daemon.*(youngest mtime wins) - Atomically installs it to
/usr/bin/dvb-warppool-daemon(withinstall -m 755) - Moves the backup to
<backup>.applied-<timestamp>so it isn’t picked again as a rollback source (prevents restart loops) systemctl restart --no-block dvb-warppool.service
- Finds
- Daemon now starts with the old binary — back online.
- Operator sees in the journal:
journalctl -u dvb-warppool-rollback.service
Configuration via env-vars
In /etc/default/dvb-warppool (or systemctl edit dvb-warppool-rollback):
WARPPOOL_BACKUP_DIR=/var/lib/dvb-warppool/backup
WARPPOOL_TARGET_BIN=/usr/bin/dvb-warppool-daemon
WARPPOOL_SERVICE=dvb-warppool.service
All have sensible defaults — only override if the deployment deviates from the standard.
Creating a backup
Rollback only works if a backup was saved before the update. So set
backup_path in the POST /api/admin/update body:
{
"asset": "x86_64-unknown-linux-gnu",
"sha256": "...",
"target_path": "/usr/bin/dvb-warppool-daemon",
"backup_path": "/var/lib/dvb-warppool/backup/daemon.v1.4.0"
}
The atomic_swap function moves the current binary to backup_path
before installing the new one — so on a crash rollback.sh has
something to restore.
Testing the helper (local, without systemd)
tmp=$(mktemp -d)
bin=$(mktemp -d)
echo "OLD" > "$tmp/daemon.v0.1.0"
echo "CRASHY" > "$bin/dvb-warppool-daemon"
# mock systemctl so no real restart runs
printf '#!/bin/bash\necho mocked\n' > "$tmp/systemctl"
chmod +x "$tmp/systemctl"
PATH="$tmp:$PATH" \
WARPPOOL_BACKUP_DIR="$tmp" \
WARPPOOL_TARGET_BIN="$bin/dvb-warppool-daemon" \
packaging/systemd/rollback.sh
cat "$bin/dvb-warppool-daemon" # → OLD
Phase 8g — Cosign-verify integrated
POST /api/admin/update can optionally run a cosign verify-blob
subprocess invocation before the sha256 check. GitHub-era releases (≤ v1.15.0) used sigstore’s keyless OIDC flow;
since the move to the self-hosted forge, verification uses the
project cosign key (cosign.pub in the repo root).
Why subprocess instead of pure-rust sigstore
The pure-rust sigstore crate would pull in 30+ transitive deps
(ASN.1, X.509, certificate-chain parser, rekor client). A cosign CLI
subprocess is:
- Transparent — the operator sees the exact command in the audit log
- Current — cosign updates come from sigstore, not from us
- Minimal — no extra Rust deps
Trade-off: the operator must have cosign installed.
Activation
Two prerequisites:
- Server-side env-var
WARPPOOL_COSIGN_BIN=/usr/local/bin/cosignset (typically in the systemd unit’sEnvironment="..."). - Request body
cosign_verify: true+cosign_args: [...].
If cosign_verify=true but the env-var is not set → 500
“WARPPOOL_COSIGN_BIN env-var not set”. Deliberately hard-fail so the
operator doesn’t get a false sense of security.
Example request
curl -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"asset": "x86_64-unknown-linux-gnu",
"sha256": "abcd1234...",
"target_path": "/usr/bin/dvb-warppool-daemon",
"backup_path": "/var/lib/dvb-warppool/backup/daemon.v1.4.0",
"cosign_verify": true,
"cosign_args": [
"--bundle=/etc/dvb-warppool/SHA256SUMS.bundle",
"--key=/etc/dvb-warppool/cosign.pub"
]
}' \
http://localhost:18334/api/admin/update
Internally (after the asset download) the server fetches the release’s
signed SHA256SUMS asset and runs cosign verify-blob on that file
— the release signs only SHA256SUMS, not each individual asset, so
SHA256SUMS is the blob the signature covers. cosign verify-blob
checks the signature against its last (blob) argument, so that argument
must be SHA256SUMS, never the asset. Once the signature checks out the
server binds the asset to it: the downloaded asset’s sha256 (the
sha256 you passed) must appear as that asset’s line in the now
signature-verified SHA256SUMS, otherwise the update is refused (403):
# the blob (last arg) is the SHA256SUMS file the release signed —
# NOT the asset; per-asset integrity comes from its line inside it.
$WARPPOOL_COSIGN_BIN verify-blob \
--bundle=<path to SHA256SUMS.bundle> \
--key=<path to cosign.pub> \
<downloaded SHA256SUMS>
Note that cosign_args are passed through verbatim, so the bundle (or, for an
archived GitHub-era release, a detached --signature) has to be reachable from
the daemon — a local path or a URL cosign can fetch.
If cosign exits ≠ 0 → the downloaded asset (and the SHA256SUMS temp)
are deleted, response 403, audit update.failed with
cosign verify-blob failed: exit N. The same 403 fires if the asset’s
sha256 is not listed in — or mismatches — the verified SHA256SUMS.
Security model
| Layer | What is checked |
|---|---|
sha256 | File integrity against the hash supplied by the operator |
cosign verify-blob | Signature authenticity against the project cosign key (cosign.pub) |
cosign_args | Fully operator-controlled — the server only appends the downloaded SHA256SUMS file as the last (blob) arg |
| Audit trail | update.applied / update.failed with details |
Defense in depth: sha256 alone doesn’t protect against a “malicious operator with the correct hash”; cosign alone doesn’t protect against bit-drift during download. Both together are the right production configuration.
Phase 8 — completed
| Sub | What |
|---|---|
| 8a | mdBook Documentation Site |
| 8b | Reproducible Builds (lto=fat + –remap-path-prefix + repro-CI) |
| 8c | Auto-Update Foundation Crate + CLI |
| 8d | Auto-Update API (GET update-check + POST update) |
| 8e | Periodic Auto-Check + UpdateAvailable SSE event |
| 8f | Systemd OnFailure-Rollback (StartLimitBurst + Hook + rollback.sh) |
| 8g | Cosign-Verify integrated into POST /api/admin/update |
Platform availability
Since the move to the self-hosted forge, CI runs on the project’s own runners — and there is currently neither a macOS nor a Windows runner. That changes what a release contains:
| Platform | Asset | Built by |
|---|---|---|
| Linux x86_64 / aarch64 | .tar.gz, .deb, .rpm, .AppImage | CI, every release |
| Docker (amd64 + arm64) | image on git.warppool.org | CI, every release |
| macOS Intel / Apple Silicon | .tar.gz, .dmg | built manually on a Mac and attached to the release afterwards |
| Windows x86_64 | .msi | paused — no Windows runner |
Consequences for auto-update:
- macOS assets can lag the release. They are attached shortly after publish,
not at publish time, so a macOS host that checks for an update within that
window sees the new tag but not yet its asset. The
binariesjob deliberately does not depend on the macOS build, so a Mac being offline never blocks a release. Because the manual step re-signs the checksums,SHA256SUMSandSHA256SUMS.bundleare replaced when the macOS assets land — re-fetch both if you grabbed them before. - Windows has no asset at all right now. The CLI’s
x86_64.msihost mapping and the WiX packaging sources (packaging/windows/,cargo-wix) are kept maintained, and the.msireturns as soon as a Windows runner is registered. Until then, Windows users build from source. - macOS GUI installs go through the
.dmg1-click flow (POST /api/admin/update/install-macos-dmg) once that asset is present.
Other limitations today
- No diff updates (full binary only). At 15 MB / release that’s harmless.
- Cosign-verify is operator-side (see flow above), not built into the CLI — deliberately, so the operator sees the trust anchor.
See also
- Notifications —
update_availableSSE event + UpdateBanner - Observability —
warppool_build_infolabel gauge shows the active version - Troubleshooting — sha256 mismatch, cosign failure, rollback cycle