omnifeed

ADR 0004: Releases are signed keylessly with cosign

Context

omnifeed is public, and others pull its release archives and the kinorai/omnifeed images. Each release ships checksums.txt, and the release notes say it “contains SHA-256 hashes for every artifact in this release”. That proves an archive was not corrupted in transit. It does not prove who built it: anyone who can replace the archives can replace checksums.txt too. SBOMs are already generated (goreleaser sboms: with syft), but nothing was signed: the release job had permissions: contents: write only, and no step used cosign or artifact attestations.

Decision

goreleaser signs every release with keyless cosign (Sigstore):

The identity is the release workflow on main: the versioning and release jobs run only on main. A looser match, such as a regexp on the repo URL, would also accept a signature made by any workflow on any branch of the repo.

To verify an archive:

cosign verify-blob --bundle checksums.txt.sigstore.json \
  --certificate-identity https://github.com/kinorai/omnifeed/.github/workflows/ci.yml@refs/heads/main \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  checksums.txt
sha256sum --ignore-missing -c checksums.txt

To verify an image:

cosign verify kinorai/omnifeed:<version> \
  --certificate-identity https://github.com/kinorai/omnifeed/.github/workflows/ci.yml@refs/heads/main \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Options considered

Option Why not chosen
cosign with a stored key pair A long-lived secret to keep in the repo’s secrets and rotate. Keyless gives the same check with nothing to leak
GitHub artifact attestations (actions/attest-build-provenance) Also keyless. goreleaser’s documented path is cosign, which signs the archives and the images from the same config, and the check needs no GitHub CLI
No signing, checksums only Integrity without origin, as before

Consequences

Sources