Skip to main content

Provenance, signatures and SBOM

Everything we publish is signed, and every signature is verifiable by you without asking us for a key. This page also includes the one case where verification cannot follow the artifacts.

What is signed, and how

ArtifactSignedAttested
Install kit tarballDetached cosign bundle, published beside it
Container imagescosign sign --recursive, so the index and each per-architecture manifestSPDX JSON SBOM, one per architecture
Helm chartscosign sign on the OCI chart artifactSPDX JSON SBOM

Keyless, with GitHub OIDC. There is no long-lived private key to steal, and no public key for you to fetch and trust. Each signature is bound to a workflow identity recorded in a public transparency log, so verification asserts who built it, not who holds a key.

SBOMs are generated with syft and attached as SPDX JSON attestations.

SLSA provenance is deliberately off

BuildKit's provenance attestations add manifests to the multi-architecture index carrying a placeholder platform. That breaks tools which walk an index by platform, including our own mirroring. We ship the SPDX SBOM attestation instead, which is the thing customers actually verify. If you require SLSA provenance, tell us: it is a build-flag change plus mirroring work, not a redesign.

Verify the install kit

Do this before you unpack it. It is step 3 of Prepare the artifacts.

cosign verify-blob \
--bundle cerebellum-install-kit.tar.gz.cosign.bundle \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github\.com/gen0sec/cerebellum/\.github/workflows/release\.yaml@refs/tags/' \
cerebellum-install-kit.tar.gz

Both certificate flags are required. cosign refuses to verify without an expected issuer and an expected identity, so a shortened command fails rather than passing weakly.

The identity is pinned to one workflow, on a tag reference. A looser pattern would accept a signature produced by any workflow, on any branch, in our organization. That is the difference between "built by our release pipeline from a tag" and "built by somebody at Gen0Sec".

Verify an image or a chart

Against our registry, or against yours after mirroring:

cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github\.com/gen0sec/cerebellum/\.github/workflows/' \
registry.gen0sec.com/gen0sec/auth-api:0.1.0

Read the SBOM

cosign download attestation \
--predicate-type https://spdx.dev/Document \
registry.gen0sec.com/gen0sec/auth-api:0.1.0 \
| jq -r '.payload | @base64d | fromjson | .predicate'

The predicate is an SPDX JSON document. Feed it to whatever consumes SBOMs in your pipeline.

Verification across the air gap

This is the part nobody else documents, so it gets its own section.

What crosses, and what does not

Signature verification needs the public transparency log, and an isolated network does not have one. So the bundler verifies once, on the connected staging host, before packaging, and protects everything downstream with checksums.

connected staging host isolated network
────────────────────── ────────────────
pull every artifact by digest
cosign verify each one (cannot re-verify: no transparency log)
write SHA256SUMS -> verify-package re-checks SHA256SUMS
print the bundle fingerprint -> mirror re-checks it before pushing
package
StageGuarantee
Before packagingFull cryptographic signature verification, every image and every chart
The tarballSHA256SUMS over every file, plus a fingerprint, which is a sha256 of that list
On the isolated sideverify-package and mirror both re-check SHA256SUMS and refuse to proceed on a mismatch

Record the fingerprint that verification prints on the connected side, out of band. Comparing it on the isolated side is what closes the loop between the two networks.

Signature re-verification inside the isolated network is not supported

It would need the transparency log, or a pinned trust bundle carried across and kept current. We do not pretend otherwise. If your policy requires cryptographic verification at the destination rather than checksum verification, tell us. That is a different design, and we would rather discuss it now than have you find the gap during an audit.

The one case where attestations are lost

--arch amd64 or --arch arm64 halves the transfer. It also extracts one manifest out of each multi-architecture index, which changes every image digest by definition.

Multi-architecture bundle (default)Single-architecture bundle
Image signaturesVerifyVerify. Each per-architecture manifest is signed individually
SBOM attestationsVerifyLost. They hang off the index, and the index is what is left behind
Digests match the release manifestYesNo, by definition

The bundler warns at the time and annotates the bundle rather than letting this be a silent difference. If your acceptance process checks attestations, stay on the default.

Digest pinning

release-manifest.yaml in the install kit pins every image and chart to a digest, not to a tag. Tags move. Digests do not.

That is what makes a release reproducible: the bundler resolves nothing at build time, it fetches exactly the digests the manifest names, and it fails if one is missing.

yq '.spec.images[] | [.image, .imageTag, .digest] | @tsv' $KIT/release-manifest.yaml

If verification fails

SymptomCause
no matching signaturesWrong identity pattern, or the artifact is not ours
Certificate fetch errorsThe Sigstore hosts are blocked. See Network and connectivity
The image verifies, the SBOM does notSingle-architecture bundle. Expected, see above
SHA256SUMS mismatch on the isolated sideThe transfer altered the bundle. Re-transfer, do not proceed

A failed verification is not a step to skip. The bundler has a flag that drops the check, and a bundle built with it is annotated as unverified. Use it only if cosign genuinely cannot be installed on your staging host, and record that you did.