Skip to main content

ADR-0019: Supply chain and release engineering

Status

Accepted

Context

Phase 7 ships v0.1.0. Everything routeD loads or that operators run must be verifiable: container images, the Helm chart, and model artifacts (which can silently change routing if swapped, ADR-0016). The oci:// artifact scheme has been reserved since ADR-0016; releases need signing, SBOMs and a repeatable pipeline.

Decision

oci:// artifact resolution (crates/artifact)

  • URI: oci://<registry>/<repository>@sha256:<manifest digest>. Tags are rejected: the digest pin is the root of trust, exactly as for https://.
  • Flow: fetch the manifest by digest (OCI and Docker v2 media types), verify the manifest bytes hash to the pinned digest, then fetch the artifact layer blob and verify it against the layer digest named by the now-trusted manifest. Multi-layer images are rejected; a routeD model artifact is a single-layer OCI artifact (oras push produces one).
  • Auth: anonymous first; on 401 the standard WWW-Authenticate: Bearer token dance (realm/service/scope) with an anonymous token request, which covers public GHCR / Docker Hub artifacts. Private registries land with credential support later; the URI shape does not change.
  • Transport is always HTTPS except for localhost / 127.0.0.1 (registries in tests and kind clusters), mirroring container-runtime convention.
  • Cache key: the pinned manifest digest (content-addressed as before); cache hits re-verify.

Image signing and SBOMs

  • Release images (routed, routed-operator) are signed with cosign keyless (GitHub OIDC) in the release workflow; consumers verify with cosign verify --certificate-identity-regexp against the repository's workflow identity.
  • SBOMs are CycloneDX, generated by cargo cyclonedx (installed in the toolchain image; make sbom writes sbom/<crate>.cdx.json for the three binaries) and attached to the GitHub release. cargo deny remains the advisory/license gate in CI.

Release process

  • Versioning: the workspace version is the release version (pre-1.0, breaking changes bump the minor). The snapshot embeds the compiler version, so a release bump changes every golden hash; regenerating them is part of the release commit.
  • Cut: update the workspace and chart appVersion, cut the CHANGELOG section, regenerate goldens, run the full gate (make ci, make onnx, make e2e), commit, tag vX.Y.Z.
  • The tag triggers .github/workflows/release.yml: build and push multi-arch images to GHCR, sign them, generate SBOMs, package and push the chart as an OCI artifact, and publish a GitHub release with checksummed artifacts. The workflow has id-token: write for keyless signing and packages: write for GHCR only.

Consequences

  • A model artifact, an image and the chart are all fetched by digest or verified by signature; no unpinned artifact reaches a cluster through routeD's own mechanisms.
  • Release builds happen only in CI from a tag; local builds stay -dev and are never signed.
  • The threat model (docs/threat-model.md) records what signing does and does not defend against.

Alternatives considered

  • A full OCI client crate (oci-distribution): rejected for now; the pull-by-digest read path is ~150 lines over the existing ureq and avoids a large dependency surface in a security-sensitive crate.
  • Keyed (non-keyless) cosign signing: rejected; key management burden with no verification benefit for a public open-source project.
  • SPDX SBOMs: CycloneDX chosen for first-class Rust tooling; the format can be widened later if consumers need SPDX.