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 forhttps://. - 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 pushproduces one). - Auth: anonymous first; on 401 the standard
WWW-Authenticate: Bearertoken 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 withcosign verify --certificate-identity-regexpagainst the repository's workflow identity. - SBOMs are CycloneDX, generated by
cargo cyclonedx(installed in the toolchain image;make sbomwritessbom/<crate>.cdx.jsonfor the three binaries) and attached to the GitHub release.cargo denyremains 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, tagvX.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 hasid-token: writefor keyless signing andpackages: writefor 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
-devand 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 existingureqand 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.