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.

#!/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 via Ord. Pre-release < stable (semver convention).
  • release — GitHub releases API with minimal serde deserialization (no octocrab — that would be 30+ transitive deps for 3 fields).
  • downloaddownload_verified(client, url, dest, expected_sha) with streaming write + SHA-256 accumulator. Mismatch → file deleted.
  • swapatomic_swap(new, current, backup_to) with POSIX rename (atomic on the same filesystem). Sets chmod 0755 on the new binary, optionally backs up the old one to backup_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

  1. Daemon starts with the new binary.
  2. Crash within 5s → systemd Restart=on-failure retries.
  3. After 4 crashes in 5 minutes → systemd gives up and triggers OnFailure=dvb-warppool-rollback.service.
  4. rollback.sh:
    • Finds /var/lib/dvb-warppool/backup/daemon.* (youngest mtime wins)
    • Atomically installs it to /usr/bin/dvb-warppool-daemon (with install -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
  5. Daemon now starts with the old binary — back online.
  6. 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:

  1. Server-side env-var WARPPOOL_COSIGN_BIN=/usr/local/bin/cosign set (typically in the systemd unit's Environment="...").
  2. 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

LayerWhat is checked
sha256File integrity against the hash supplied by the operator
cosign verify-blobSignature authenticity against sigstore OIDC issuer + identity regex
cosign_argsFully operator-controlled — the server only appends the downloaded file as the last arg
Audit trailupdate.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

SubWhat
8amdBook Documentation Site
8bReproducible Builds (lto=fat + --remap-path-prefix + repro-CI)
8cAuto-Update Foundation Crate + CLI
8dAuto-Update API (GET update-check + POST update)
8ePeriodic Auto-Check + UpdateAvailable SSE event
8fSystemd OnFailure-Rollback (StartLimitBurst + Hook + rollback.sh)
8gCosign-Verify integrated into POST /api/admin/update

Limitations today

  • macOS .dmg and Windows .msi are 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