Skip to content

ci: sign Docker images and attach build provenance - #4451

Draft
stevenvegt wants to merge 3 commits into
masterfrom
ci/cosign-keyless-signing
Draft

ci: sign Docker images and attach build provenance#4451
stevenvegt wants to merge 3 commits into
masterfrom
ci/cosign-keyless-signing

Conversation

@stevenvegt

@stevenvegt stevenvegt commented Aug 12, 2026

Copy link
Copy Markdown
Member

What

Adds Sigstore keyless signing and SLSA provenance/SBOM attestations to the Docker image build workflow, plus user documentation (deployment section) covering manual verification, digest pinning, and enforcement in Kubernetes/AKS and Azure DevOps pipelines. Images pushed to Docker Hub (master and release tags) are signed with cosign using the workflow's GitHub Actions OIDC identity.

Why

Today, possession of the Docker Hub credentials is the only thing standing between users and a locally built image published under our name. With keyless signing, the signature certificate names the repository, workflow file, ref and commit. A signature with that identity can only be produced by the workflow running on GitHub's infrastructure, not by anyone holding registry credentials. Every signature is also recorded in the public Rekor transparency log, so signing events are auditable after the fact.

Users can verify a published image with:

cosign verify nutsfoundation/nuts-node:<tag> \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com \
  --certificate-identity-regexp '^https://github.com/nuts-foundation/nuts-node(-private)?/\.github/workflows/build-images\.yaml@'

The identity regexp also accepts nuts-foundation/nuts-node-private, the private clone that embargoed security releases are built from; rejecting that identity would make enforcing verifiers block exactly the security releases users most need to install. The new docs page (docs/pages/deployment/verifying-images.rst) expands on this with digest pinning for docker-compose, a Kyverno ClusterPolicy for cluster-side enforcement, and an Azure DevOps pipeline gate.

Design notes

  • Signing runs in a separate job. GitHub does not issue OIDC tokens to workflows triggered from forked pull requests, and keeping id-token: write out of the build job means none of the third-party build actions ever see the OIDC credential.
  • provenance: mode=max embeds BuildKit SLSA provenance (source repo, commit, build args) in the pushed image index; sbom: true adds an SPDX SBOM. On their own these are unsigned metadata; the cosign signature over the index digest is what makes them tamper-evident.
  • The signature attaches to the image digest, so every tag pointing at that digest (version tag, latest, master) is covered. Signatures appear on Docker Hub as sha256-<digest>.sig pseudo-tags.
  • Signing an embargoed release from the private clone publishes that repository's name and the release timing in the Fulcio certificate and the Rekor log at signing time. Since the image lands on Docker Hub at the same moment, this leaks little beyond "this release came via the security pipeline".
  • The :dev convenience image is intentionally left unsigned for now.
  • Immutable tags for releases have been enabled on Docker Hub separately (protects against token theft and external actors; not a control against org admins).

Validation

The workflow change cannot be fully exercised from a PR build: the sign job is skipped on pull_request events and PR builds do not push. The first real validation is the workflow run on master after merge; that run should be watched and the resulting master image verified with the command above. The documentation was built locally with the docs Docker image; the new page compiles without warnings.

Follow-ups

  • Apply the same workflow change to nuts-foundation/nuts-node-private, so embargoed releases are signed. Until that is done, an embargoed release ships unsigned and fails verification.

Assisted-by: AI

Images pushed to Docker Hub are now signed with cosign using the
GitHub Actions OIDC identity (Sigstore keyless), and BuildKit SLSA
provenance and SBOM attestations are embedded in the image index.
This lets users verify that a published image was built by this
repository's CI workflow, rather than pushed manually with Docker
Hub credentials.

Signing runs in a separate job because GitHub does not issue OIDC
tokens to workflows triggered from forked pull requests, and to
avoid exposing the OIDC credential to third-party build actions.

Assisted-by: AI
@qltysh

qltysh Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Qlty


Coverage Impact

This PR will not change total coverage.

🚦 See full report on Qlty Cloud »

🛟 Help
  • Diff Coverage: Coverage for added or modified lines of code (excludes deleted files). Learn more.

  • Total Coverage: Coverage for the whole repository, calculated as the sum of all File Coverage. Learn more.

  • File Coverage: Covered Lines divided by Covered Lines plus Missed Lines. (Excludes non-executable lines including blank lines and comments.)

    • Indirect Changes: Changes to File Coverage for files that were not modified in this PR. Learn more.

Adds a deployment page showing how to verify image signatures with
cosign, deploy by digest, and enforce verification with Kyverno on
Kubernetes/AKS and in Azure DevOps pipelines.

Assisted-by: AI
Embargoed security releases are built from the private clone
nuts-foundation/nuts-node-private, so their signature carries that
repository's workflow identity. Accepting only the public identity
would make enforcing verifiers reject exactly the security releases
users most need to install.

Assisted-by: AI
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants