Skip to content

Blog

How deputyOS images are signed (minisign + cosign + SLSA)

The full trust chain for an deputyOS image: how it's built reproducibly, signed twice, attested, and verified at apply time.

6 min read deputyOS team

Supply-chain security for an appliance image is its own discipline. deputyOS treats it as load-bearing: a successful attack on our build pipeline could compromise every device that updates from our CDN. This post walks through what we ship to prevent that, what we verify on-device, and the gaps we’re still closing.

The shape of the trust chain

There are four things being signed at different points:

  1. The image artefact (.qcow2, .img.xz, etc.) — signed with minisign.
  2. The CycloneDX SBOM — describes every package, runtime, model, and config baked in.
  3. The SLSA v1 provenance — describes the build environment (commit, builder, materials, parameters).
  4. The release manifest — the JSON that points at all of the above. Signed separately so the manifest itself is tamper-evident.

A device performing deputyctl update --check walks this chain in reverse.

Why minisign

minisign is a small, modern Ed25519-based signing format from Frank Denis. We picked it over OpenPGP for three reasons:

  • No PKI, no keyserver dance. The public key fits in a single line; it ships with the image and with the picker page. No “trust this third-party keyserver” step.
  • Fast verification on Pi-class hardware. Ed25519 verification is one of the cheapest signature schemes; a Pi 5 verifies the whole manifest in milliseconds.
  • Small signatures. .minisig files are tiny — manifest signatures are 158 bytes.

The key pair lives offline. CI gets a release-mode key (rotated periodically). Contributors get dev keys for local builds; dev-key signatures are accepted only when explicitly opted into.

Why cosign on top

cosign gives us SLSA-aware tooling, ecosystem familiarity, and the ability for downstream consumers (e.g. enterprise verifiers, package archives) to use existing sigstore-aware tools rather than rolling minisign support.

The cosign signature is alongside, not instead of, the minisign signature. Both are required for an update to apply by default.

What’s in the SBOM

CycloneDX 1.5 JSON, generated by make sbom. The SBOM enumerates:

  • Every Debian package on the image (name, version, license, SHA256 of the deb).
  • Every Node module in node_modules/ per profile.
  • Every Python wheel in the agent’s venv.
  • Every model file baked: name, SHA256, license, source URL.
  • The kernel version + config hash.
  • The ClamAV signature DB version.
  • The Magika model version.
  • deputyctl + deputywizard + deputypwa binary SHA256s.

Anyone consuming an deputyOS image can cat /etc/deputyos/sbom.json (the SBOM is baked into the image alongside the build metadata) and audit the entire content.

What’s in the SLSA v1 provenance

The in-toto attestation describes the build:

  • Git commit SHA of the source repo.
  • Build host identity (CI: github.com/deputyos/deputyos run-id N).
  • All --extra-vars passed to Packer.
  • All inputs the build consumed (source tarball SHAs, base image SHAs, model file SHAs).
  • The build’s start and end timestamp.

Combined with the SBOM, this is enough to re-execute the build on an independent host and check the resulting image SHA matches. That’s the release-gate test we’re working toward.

How deputyctl update verifies the chain

When you run deputyctl update --check:

  1. Fetch cdn.deputyos.com/<channel>/manifest.json and manifest.json.minisig.
  2. Verify the manifest signature with the bundled public key (baked into the image at /etc/deputyos/pubkey.minisign). Refuse to proceed on mismatch.
  3. The manifest lists each available artefact + its SHA256 + signature URLs. Compare your current image’s build.json against the manifest to see if a new version exists.
  4. On --apply: download the new artefact. Verify its SHA256 matches the manifest. Verify its .minisig signature against the bundled public key. Verify its .sigstore cosign signature against the bundled cosign verification cert.
  5. Stage the new image into the inactive A/B partition.
  6. Trigger the bootloader to flip the active slot pointer for the next boot.
  7. Reboot.
  8. The watchdog gives the new image 5 minutes to come up healthy. If not — automatic rollback.

There is no “trust on first use” anywhere. The public keys travel with the image; if your image is genuine, your update path is genuine.

The reproducibility goal

SLSA L3 requires that an independent third party can rebuild the same image and get a byte-identical output. That implies:

  • Build is hermetic (no apt update that pulls today’s package versions; everything pinned).
  • Build is deterministic (no current-timestamp embedding, sorted file outputs, fixed mtimes).
  • Inputs are precisely specified (SHAs everywhere).

We do the first two today. The third is mostly done — the open piece is that we don’t yet have a second, independent build host running the same workflow against the same commit. That comparison is the proof. Adding a second runner is in flight (a release gate).

Once that’s in place, anyone can:

git clone https://github.com/deputyos/deputyos
git checkout v0.3.0
make verify VERSION=2026.5.x TARGET=qemu-aarch64

make verify rebuilds the image locally and SHA-compares against the published artefact. Today the published artefact doesn’t exist (no public CDN), so the verifier is staged; once v0.3.0 publishes, this should work for any user.

Sneakernet update verification

The same chain works without internet:

# On a trusted machine
curl -O https://cdn.deputyos.com/stable/manifest.json
curl -O https://cdn.deputyos.com/stable/manifest.json.minisig
minisign -Vm manifest.json -P RWQ...
# Download the artefacts listed in the manifest.

# Copy to USB. On the air-gapped device:
deputyctl update --from /mnt/deputyos/<usb>/manifest.json

Same signature verification, same SHA check, same A/B swap. The on-device path doesn’t care whether the bytes arrived from the public CDN or a thumb drive.

What we don’t sign (and why)

  • The wizard’s outbound webhook calls to your channels (Telegram, Slack, etc.). Those use the per-channel auth (bot tokens, signing secrets) that you provide.
  • Conversation history written to the data partition. This is your data; we don’t co-sign it. If you want a tamper-evident log, hook into post-message and write your own hash chain.
  • Backups to your B2/R2 bucket. Same reason. You can extend deputyctl backup with a pre-upload signing step if you want.

The signing chain is for deputyOS as shipped — the image, the manifest, the updates. User data and channel state are outside its scope by design.

What an attacker would have to compromise

Walking the chain in the attacker’s direction:

  1. To ship a malicious image, attacker needs the release minisign key + the cosign key + a way into our CDN bucket.
  2. To poison deputyctl update, attacker needs at minimum the minisign key (the cosign signature is verified too, so a single-key compromise isn’t enough).
  3. To trick the manifest, attacker needs the manifest-signing key (same chain).
  4. To attack reproducibility, attacker needs to compromise two independent build hosts simultaneously.

The release key never touches a developer laptop or a CI runner’s environment as plaintext — CI signs by hashing the artefact and submitting the hash to a key-holding service. (The exact key-holding mechanism is a separate document we’ll publish before public release; until then, builds are signed with the dev key.)

What’s still ahead

  • External audit of the trust chain (not yet commissioned).
  • Second build host for SLSA L3 verification (a release gate).
  • cdn.deputyos.com public (M4a milestone).
  • security@deputyos.com mailbox with 90-day disclosure SLA, published PGP key, security.txt at /.well-known/security.txt.

Until those land, the trust chain is sound by construction but not externally validated. If you’re shipping deputyOS in production today, treat this like any pre-1.0 piece of software — audit the SBOM yourself, lock down ingress with Tailscale, and don’t expose 8088 to the public internet.

The security defaults doc covers the on-device side; reporting vulnerabilities covers the disclosure side.

Related posts

Search

Search index isn't built yet. Run npm run build to generate it (Pagefind writes to dist/pagefind/).