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
| Artifact | Signed | Attested |
|---|---|---|
| Install kit tarball | Detached cosign bundle, published beside it | |
| Container images | cosign sign --recursive, so the index and each per-architecture manifest | SPDX JSON SBOM, one per architecture |
| Helm charts | cosign sign on the OCI chart artifact | SPDX 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.
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
| Stage | Guarantee |
|---|---|
| Before packaging | Full cryptographic signature verification, every image and every chart |
| The tarball | SHA256SUMS over every file, plus a fingerprint, which is a sha256 of that list |
| On the isolated side | verify-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.
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 signatures | Verify | Verify. Each per-architecture manifest is signed individually |
| SBOM attestations | Verify | Lost. They hang off the index, and the index is what is left behind |
| Digests match the release manifest | Yes | No, 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
| Symptom | Cause |
|---|---|
no matching signatures | Wrong identity pattern, or the artifact is not ours |
| Certificate fetch errors | The Sigstore hosts are blocked. See Network and connectivity |
| The image verifies, the SBOM does not | Single-architecture bundle. Expected, see above |
SHA256SUMS mismatch on the isolated side | The 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.