Auto-Update
Auto-update runs in two phases: 8c Foundation (this doc) provides update-check + verified download as CLI subcommands; 8d Wiring (roadmap) brings atomic swap + health check + rollback into the daemon itself.
Current MVP (Phase 8c)
Two CLI commands, manually triggered by the operator:
dvb-warppool-cli check-update
Fetches the latest release from GitHub and compares it against the
installed version (CARGO_PKG_VERSION from the CLI binary).
$ dvb-warppool-cli check-update
Installed: 0.1.0
Latest: v0.2.0 (Release 0.2.0)
Update: 0.1.0 → 0.2.0 (available)
Release-Assets:
· dvb-warppool-daemon-linux-x86_64 (12345678 bytes)
· dvb-warppool-daemon-linux-aarch64 (11456789 bytes)
· SHA256SUMS (256 bytes)
· SHA256SUMS.sig (96 bytes)
Optional --json for scripting. --repo owner/name for forks or tests.
dvb-warppool-cli download-update
Downloads a release asset with sha256 verification. Performs no atomic swap and no restart — that's the operator's job, so the workflow stays auditable.
# Latest binary for the local host (auto-detected linux-x86_64/-aarch64/macos)
$ dvb-warppool-cli download-update \
--to /tmp/dvb-warppool-daemon.new \
--sha256 abcd1234... # aus SHA256SUMS
Download: dvb-warppool-daemon-linux-x86_64 (12345678 bytes) → /tmp/dvb-warppool-daemon.new
✓ 12345678 bytes written, sha256=abcd1234...
→ Manual atomic swap (Phase 8d wires this into the daemon):
chmod +x /tmp/dvb-warppool-daemon.new
sudo mv /tmp/dvb-warppool-daemon.new /usr/local/bin/dvb-warppool-daemon
sudo systemctl restart dvb-warppool
On sha256 mismatch the half-written file is deleted — no "tainted download" left lying around.
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://github.com/dvb-projekt/dvb-WarpPool/releases/download/$TAG/SHA256SUMS
curl -sSfLO https://github.com/dvb-projekt/dvb-WarpPool/releases/download/$TAG/SHA256SUMS.sig
cosign verify-blob \
--certificate-identity-regexp "https://github.com/dvb-projekt/dvb-WarpPool/.github/workflows/release.yml@.*" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--signature SHA256SUMS.sig SHA256SUMS
# extract the expected sha256 for our asset
ASSET="dvb-warppool-daemon-linux-x86_64"
EXPECTED=$(awk -v a="$ASSET" '$2 == a {print $1}' SHA256SUMS)
# download + verify
dvb-warppool-cli download-update --to /tmp/$ASSET --sha256 "$EXPECTED"
# manual swap (Phase 8d automates this)
sudo install -m 755 /tmp/$ASSET /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— GitHub releases API 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": "0.1.0",
"latest": "v0.2.0",
"name": "Release 0.2.0",
"prerelease": false,
"newer": "0.2.0",
"assets": [
{"name": "dvb-warppool-daemon-linux-x86_64", "size": 12345678, "url": "..."},
{"name": "SHA256SUMS", "size": 256, "url": "..."}
]
}
Audit: update.check with the resolved latest tag as target.
POST /api/admin/update
Downloads the chosen asset, verifies sha256, atomic-renames into
target_path. 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": "linux-x86_64",
"sha256": "abcd1234...",
"target_path": "/usr/local/bin/dvb-warppool-daemon",
"backup_path": "/var/lib/dvb-warppool/backup/daemon.v0.1.0"
}' \
http://localhost:18334/api/admin/update
{
"tag": "v0.2.0",
"asset": "dvb-warppool-daemon-linux-x86_64",
"bytes": 12345678,
"sha256": "abcd1234...",
"target_path": "/usr/local/bin/dvb-warppool-daemon",
"backup_path": "/var/lib/dvb-warppool/backup/daemon.v0.1.0",
"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 GitHub 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. The GitHub API
allows 60 unauthenticated requests/h per IP — with the default 24h
interval we land at <2 requests/day, well below the limit.
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": "linux-x86_64",
"sha256": "...",
"target_path": "/usr/bin/dvb-warppool-daemon",
"backup_path": "/var/lib/dvb-warppool/backup/daemon.v0.1.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. This matches sigstore's
keyless OIDC flow that our
release.yml has been using
since Phase 6.
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": "linux-x86_64",
"sha256": "abcd1234...",
"target_path": "/usr/bin/dvb-warppool-daemon",
"backup_path": "/var/lib/dvb-warppool/backup/daemon.v0.1.0",
"cosign_verify": true,
"cosign_args": [
"--signature=https://github.com/dvb-projekt/dvb-WarpPool/releases/download/v0.2.0/SHA256SUMS.sig",
"--certificate=https://github.com/dvb-projekt/dvb-WarpPool/releases/download/v0.2.0/SHA256SUMS.pem",
"--certificate-identity-regexp=https://github.com/dvb-projekt/dvb-WarpPool/.github/workflows/release.yml@.*",
"--certificate-oidc-issuer=https://token.actions.githubusercontent.com"
]
}' \
http://localhost:18334/api/admin/update
Internally (after the download) the server runs cosign verify-blob
on the downloaded SHA256SUMS file. The release only signs that file —
per-binary integrity is then established by matching the binary's
sha256 against its line in SHA256SUMS:
$WARPPOOL_COSIGN_BIN verify-blob \
--signature=... --certificate=... \
--certificate-identity-regexp='https://github.com/dvb-projekt/dvb-WarpPool/.github/workflows/release.yml@.*' \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
/tmp/SHA256SUMS
If cosign exits ≠ 0 → the downloaded file is deleted, response 403,
audit update.failed with cosign verify-blob failed: exit N.
Security model
| Layer | What is checked |
|---|---|
sha256 | File integrity against the hash supplied by the operator |
cosign verify-blob | Signature authenticity against sigstore OIDC issuer + identity regex |
cosign_args | Fully operator-controlled — the server only appends the downloaded file as the last 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 |
Limitations today
- macOS
.dmgand Windows.msiare not raw binaries — the operator can only update those via the respective native installers. Auto-update is Linux-first. - 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