Skip to content

ci: sign release artifacts with cosign and attach SPDX SBOMs - #824

Open
Yetkin Timocin (ytimocin) wants to merge 2 commits into
ci/release-orchestratorfrom
ci/release-signing
Open

ci: sign release artifacts with cosign and attach SPDX SBOMs#824
Yetkin Timocin (ytimocin) wants to merge 2 commits into
ci/release-orchestratorfrom
ci/release-signing

Conversation

@ytimocin

@ytimocin Yetkin Timocin (ytimocin) commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #823, which is itself stacked on #745. Based on ci/release-orchestrator so the diff shows only this change; basing it on #745 directly would have included #823’s diff too. Retarget as the stack merges.

Images and the CRD bundle now carry Sigstore signatures, keyless via the release workflow’s GitHub OIDC identity — no long-lived signing key to hold or rotate. Each image also carries a per-platform SPDX SBOM attached to its index, readable without pulling the image.

Three decisions worth reviewing:

  • Signed by digest, not by tag. buildx records the digest it pushed via --metadata-file and the signing script reads it from there. Signing a tag would sign whatever that tag resolves to when cosign runs, which is not necessarily what the run built. --recursive also covers the per-platform manifests inside the index, so a verifier that already resolved to an arch-specific digest still finds a signature.
  • The work list comes from the build-metadata directory, not a hand-maintained list of image names that could drift from what is actually built and publish an unsigned image while the job still exits 0. The complementary invariant — that everything a release should contain was in fact built — stays with the verification step, which runs before anything is signed.
  • The verification identity is bound to release.yml’s job_workflow_ref, not to the repository, so another workflow that later gains id-token: write cannot mint signatures that pass. No ref constraint, so both tag pushes and workflow_dispatch verify. Any OIDC trust policy added later must be scoped the same way, never repo:...:*.

Both signing steps verify what they just produced and fail the release if it does not check out — a signature nobody can verify is worse than none, because RELEASING.md tells users to rely on it. The CRD bundle’s checksum is signed as a blob and its bundle uploaded as a third release asset, which publish-release now requires. Consumers that enumerate release assets should know about that addition.

SBOM and signing are one PR because signing by digest needs the --metadata-file plumbing the SBOM change adds; splitting them would land plumbing with no consumer.

Not yet executed end to end, and this one cannot be locally. Keyless signing needs real Actions OIDC, so the test suite covers the logic around cosign — which digest is signed, what happens when a step fails — against a stub, not cosign itself. A dry run in a scratch repo is planned before merge; the certificate SAN is the specific thing to confirm against a real Fulcio cert, since the identity pattern is strict enough that a wrong guess fails every release.

Scoped out deliberately: cosign attest --type=spdxjson and release-metadata.json, both still open in #693. The attestation question was raised for maintainer input in this comment and has not been answered; this PR proceeds without it rather than settling it.

Part of #693.

Images and the CRD bundle now carry Sigstore signatures, made keyless with
the release workflow's GitHub OIDC identity, so there is no long-lived
signing key to hold or rotate. Each image also carries a per-platform SPDX
SBOM attached to its index, readable without pulling the image.

Images are signed by digest, not by tag. buildx records the digest it
pushed via --metadata-file and the signing script reads it from there:
signing a tag would sign whatever that tag resolves to when cosign runs,
which is not necessarily what the run built. --recursive covers the
per-platform manifests inside the index as well, so a verifier that has
already resolved to a platform-specific digest still finds a signature.

The signing script derives its work list from the build metadata directory
rather than from a list of image names. A hand-maintained list can drift
from what is actually built and publish an unsigned image while the job
still exits 0 - the same failure shape as a hardcoded chart version. The
complementary invariant, that everything a release should contain was in
fact built, stays with the verification step, which now also asserts the
SBOM is really attached and runs before anything is signed.

Both signing steps verify what they just produced and fail the release if
it does not check out: a signature nobody can verify is worse than no
signature, because RELEASING.md tells users to rely on it. The identity
those checks pin is bound to this workflow file rather than to the
repository, so another workflow that later gains id-token: write cannot
mint signatures that pass, and it carries no ref constraint so both tag
pushes and workflow_dispatch runs verify. RELEASING.md documents the
byte-identical pattern for consumers.

The CRD bundle's checksum is signed as a blob and its signature bundle
uploaded as a third release asset, which publish-release now requires
before it will publish.

Part of #693.

Signed-off-by: Yetkin Timocin <ytimocin@microsoft.com>
@ytimocin
Yetkin Timocin (ytimocin) requested a review from a team as a code owner August 13, 2026 21:53
@ytimocin Yetkin Timocin (ytimocin) added process-improvement Any work related to improving project process - code changes, CI / CD or docs improvements. area/ci Issues related to ci labels Aug 13, 2026
@ytimocin
Yetkin Timocin (ytimocin) requested a balanced review from Copilot August 14, 2026 06:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds keyless Cosign signing and SPDX SBOMs to the release pipeline.

Changes:

  • Signs image digests and CRD checksums using GitHub OIDC.
  • Generates and validates per-platform image SBOMs.
  • Documents artifact verification and extends release-script tests.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
.github/workflows/release.yml Generates SBOMs and signs release artifacts.
.github/workflows/workflow-lint.yml Shellchecks all release test stubs.
.gitignore Ignores image metadata output.
Makefile Adds SBOM and build-metadata flags.
RELEASING.md Documents signed-artifact verification.
hack/release/publish-release.sh Requires the CRD signature bundle.
hack/release/sign-crd-bundle.sh Signs and verifies the CRD checksum.
hack/release/sign-images.sh Signs image digests recursively.
hack/release/test-release-scripts.sh Tests signing and release behavior.
hack/release/testdata/cosign Adds a Cosign test stub.
Suppressed comments (1)

RELEASING.md:134

  • The CRD verification command repeats the unconstrained-ref identity pattern, so it trusts release.yml signatures from arbitrary branch-selected workflow runs. Restrict it to release tags and the trusted dispatch branch in lockstep with the production checks.
  --certificate-identity-regexp '^https://github\.com/kubefleet-dev/kubefleet/\.github/workflows/release\.yml@' \

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread hack/release/sign-images.sh Outdated
# that ever gains id-token: write could otherwise mint signatures that pass. No
# ref constraint - a workflow_dispatch release signs as @refs/heads/<branch>
# while a tag push signs as @refs/tags/<tag>.
IDENTITY_PATTERN="^https://github\.com/${repo_pattern}/\.github/workflows/release\.yml@"
Comment thread hack/release/sign-crd-bundle.sh Outdated
# See sign-images.sh for why this is bound to the workflow file and why the
# repository name has its dots escaped before going into a regexp.
repo_pattern="${GITHUB_REPOSITORY//./\\.}"
IDENTITY_PATTERN="^https://github\.com/${repo_pattern}/\.github/workflows/release\.yml@"
Comment thread RELEASING.md Outdated
```bash
cosign verify \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github\.com/kubefleet-dev/kubefleet/\.github/workflows/release\.yml@' \
Comment thread .github/workflows/release.yml Outdated
Comment on lines +151 to +156
sbom="$(docker buildx imagetools inspect "${REGISTRY}/${IMAGE}:${TAG}" \
--format '{{ json (index .SBOM "linux/amd64").SPDX }}' 2>/dev/null || true)"
if [ -z "${sbom}" ] || [ "${sbom}" = "null" ]; then
echo "::error::${REGISTRY}/${IMAGE}:${TAG} has no SPDX SBOM attached"
exit 1
fi
Comment thread RELEASING.md Outdated
Comment on lines +146 to +149
`release.yml` is the only workflow holding `id-token: write`. Any OIDC trust
policy added later (cloud role assumption, trusted publishing) must be scoped to
that workflow's `job_workflow_ref`, never to `repo:kubefleet-dev/kubefleet:*`,
or it would be assumable from any workflow in the repository.
…form

The cosign identity pattern ended immediately after "@", so it accepted
release.yml run from any ref. A workflow_dispatch runs the workflow
definition from the ref it was started on, which made "any branch in the
repository" part of the trusted set. Constrain it to release tags and the
main / release-X.Y branches.

Move the pattern and the OIDC issuer into hack/release/identity.sh, read
by both signing scripts and by their tests. The tests previously
reimplemented the pattern, so they asserted a copy and would have kept
passing after the scripts drifted.

Be explicit about what this does not buy: it bounds which workflow
definitions can sign, not who can sign. Someone with write access can
still push a v* tag at a commit carrying a modified release.yml, and the
tag arm accepts it. Closing that needs a ruleset restricting who may
create v* tags, which the repository does not have today.

Signing runs after the images are pushed, so a run from an untrusted ref
would have published three images, signed them, written a Rekor entry,
and only then failed verification. Add check-signing-ref.sh as the first
step of create-draft-release, which gates every publishing job: the same
identity is now tested before anything exists to clean up.

The SBOM gate inspected only linux/amd64 while the release promises one
per platform, so an image missing its arm64 SBOM would have published
behind a green check. Iterate the platforms instead, cross-checked
against the Makefile as a set so a narrowed RELEASE_PLATFORMS fails
loudly rather than narrowing the check to match. Capture buildx's stderr
too - a registry error and a missing attestation need different fixes and
were reported identically.

RELEASING.md claimed release.yml was the only workflow holding
id-token: write; squad-docs.yml has it as well.

Tests: 49 -> 76. Mutating the issuer, dropping the pattern's anchor,
widening setup-release.yml's tag grammar, or leaving one of the two
documented patterns stale each now fail the suite; before, all four
passed.

Signed-off-by: Yetkin Timocin <ytimocin@microsoft.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci Issues related to ci process-improvement Any work related to improving project process - code changes, CI / CD or docs improvements.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants