Skip to main content

Reproducible Builds & Build Attestation

SPR ships two independent supply-chain guarantees you can check yourself:

  • Reproducible builds — building a given git commit produces byte-identical container images, so the digest we publish is one anyone can reproduce.
  • Build attestation — every published image is signed with a keyless Sigstore signature that ties it to our public GitHub Actions build. On the main release channel, SPR verifies this attestation before applying an update, so tampered or unsigned images are rejected.

The two work together: attestation proves an image came from our build pipeline, and reproducibility lets anyone confirm that pipeline actually built the source.

Reproducible builds

SPR container images are bit-for-bit reproducible: building the same git commit on any machine, on any day, produces byte-identical image digests. This applies to the super repo and to the container_template base image every service builds FROM.

Reproducibility requires that every build input is content-addressed and that the output bytes are deterministic. The levers:

  • Base images pinned by digest. ubuntu, alpine, node, the container_template runtime base, the Dockerfile frontend, and the BuildKit backend are all pinned by @sha256: (multi-arch index digests valid for amd64 and arm64).
  • apt via the Ubuntu snapshot archive. Every apt-get resolves against https://snapshot.ubuntu.com/ubuntu/<TS>/ at a fixed timestamp, freezing package versions. GPG signatures are still verified against the Ubuntu archive keyring. Docker CE (only in superd) is pinned to exact package versions from download.docker.com.
  • Toolchains downloaded and checksummed. Go and bun are fetched at pinned versions and verified against a sha256; GOTOOLCHAIN=local stops Go from fetching a different compiler mid-build.
  • Source deps pinned to commits. Everything git cloned (coredhcp, coredns and its SPR plugins, hostap, xdp-tools, godns, sprbus-json) is fetched at an immutable commit SHA rather than a moving branch or tag.
  • Deterministic Go builds. go build -trimpath strips absolute build paths; the frontend uses yarn install --frozen-lockfile.
  • Deterministic timestamps. SOURCE_DATE_EPOCH is set to the git commit time and the image exporter runs with rewrite-timestamp=true, which rewrites in-layer file timestamps so the layers are byte-identical.

Single source of truth: reproducible.env

All shared pins live in reproducible.env. build_docker_compose.sh and CI read it and thread every value into the build as a --set *.args.* override. Each Dockerfile also declares matching ARG defaults so a plain docker build of a single context is reproducible too.

Pins are regenerated with update-pins.sh, which re-resolves every value (image digests, Go/bun checksums, the snapshot timestamp, git commits) from read-only sources and rewrites both reproducible.env and the Dockerfile ARG defaults so the two never drift.

Building reproducibly

# Reproducible local build (loads single-arch images into Docker):
./build_docker_compose.sh

# Reproducible multi-arch build + push (used by CI):
./build_docker_compose.sh --push

The script sets SOURCE_DATE_EPOCH from the commit, pins the BuildKit backend, and forces the rewrite-timestamp=true exporter. CI does the same via .github/workflows/docker-image.yml.

docker compose build is not bit-for-bit — it uses the docker exporter without rewrite-timestamp, so in-layer timestamps vary. Use build_docker_compose.sh.

Verifying reproducibility

The repo's verify script builds each target twice with --no-cache and asserts the two digests match:

# Default targets: base api frontend db
./reproducible-build-verify.sh

# Specific targets:
./reproducible-build-verify.sh dns wireguard

It prints a line with the shared digest for each reproducible target, or a line if the two builds diverge, and exits non-zero on any failure. Set PLATFORM (default: your host arch) to verify a specific architecture.

Reproducibility is guaranteed per git commit, from a clean checkout — a dirty working tree (untracked or modified files) can change a build. CI builds from a clean checkout.

Build attestation

Every image our CI publishes is attested with a Sigstore keyless signature. There is no long-lived signing key: Sigstore's Fulcio CA issues a short-lived certificate to each GitHub Actions workflow run over OIDC, the signature is recorded in the Rekor public transparency log, and the resulting bundle is attached to the image digest. Verification confirms the image was built by our workflow, and the transparency-log entry can be audited by anyone.

superd verifies these attestations against a fixed identity:

  • Issuerhttps://token.actions.githubusercontent.com (GitHub Actions OIDC).
  • Subject (SAN) — the certificate's identity must match the spr-networks/super repo's .github/workflows/docker-image.yml workflow.
  • Transparency — the Sigstore trusted root is fetched via TUF and the verifier requires a signed certificate timestamp plus a Rekor inclusion proof.

Because verification is bound to our workflow identity, stealing a registry token is not enough to push a backdoored image to your router — an attacker would have to take over the GitHub Actions workflow itself, which would leave a permanent entry in a transparency log we do not control.

How updates are gated

On the main release channel, superd verifies provenance around every update (superd/code/attest.go):

  1. Before pulling, it enumerates the images the update would pull, resolves each remote digest, fetches the attestation bundle from the GitHub attestations API, and verifies it against the workflow identity. If any image fails, the update aborts before anything is downloaded.
  2. After pulling, it re-verifies the digest that was actually pulled to each local image. If a pulled digest fails verification, the update fails.

Verification results are published on the SPR bus (superd:attest:ok / superd:attest:failure) and surfaced in the app. See Verified Updates for the update flow. The dev channel is not gated this way and is not recommended outside of development.

Verifying an attestation by hand

You can reproduce the same provenance check with the GitHub CLI:

gh attestation verify \
oci://ghcr.io/spr-networks/super_base:latest \
--repo spr-networks/super

This fetches the image's Sigstore bundle and confirms it was signed by the spr-networks/super build workflow.