Skip to content

Signed GHCR image with SBOM and SLSA provenance for release tags - #866

Merged
KrzysztofPajak merged 17 commits into
developfrom
test/ghcr-private
Oct 2, 2026
Merged

KrzysztofPajak merged 17 commits into
developfrom
test/ghcr-private

Conversation

@KrzysztofPajak

@KrzysztofPajak KrzysztofPajak commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Resolves: no issue
Type: feature

Issue

There is no signed, verifiable GrandNode image in GitHub Container Registry. The first version of docker-ghcr.yml built and signed the image, then failed at Verify SBOM attestation:

Error: none of the attestations matched the predicate type: spdx, found: https://sigstore.dev/cosign/sign/v1

sbom: true / provenance: mode=max in build-push-action attach BuildKit attestations to the image index unsigned. cosign verify-attestation only accepts cosign-signed attestations, so the SBOM and provenance checks could never pass.

Solution

.github/workflows/docker-ghcr.yml builds, pushes, signs and verifies ghcr.io/grandnode/grandnode2:

  • Triggers: release tags X.Y.Z and X.Y.Z-* (e.g. 2.3.0, 2.4.0-beta) publish :<tag>; manual run with a tag input (Actions → GrandNode Docker GHCR → Run workflow) checks out and publishes any existing release tag, including tags created before this workflow existed. The tag is validated against X.Y.Z[-suffix], and the image is labelled with the commit actually built.
  • Signature: cosign keyless (Sigstore, GitHub OIDC).
  • SBOM: Syft generates SPDX JSON, which is attested with cosign attest --type spdxjson and also uploaded as a run artifact.
  • Provenance: SLSA v1 via actions/attest-build-provenance, pushed to the registry.
  • Verification in the same run: cosign verify, cosign verify-attestation and gh attestation verify, all pinned to the exact workflow identity (github.workflow_ref) instead of a refs/heads/.* regexp.
  • Hardening:
    • all actions are pinned to commit SHAs;
    • permissions are {} at workflow level, and the job gets only contents: read, packages: write, id-token: write and attestations: write;
    • checkout uses persist-credentials: false;
    • step outputs go through env rather than being interpolated into scripts;
    • a concurrency group is set.
  • BuildKit's own unsigned SBOM/provenance are turned off. GIT_COMMIT/GIT_BRANCH build args are passed the same way as in the Docker Hub workflow.

GHCR has no OCI Referrers API, so signatures and attestations show up as extra untagged package versions next to a sha256-<digest> tag. Do not prune untagged versions blindly, or verification of live images breaks.

Package visibility (private) is a GHCR package setting, not part of this workflow.

Breaking changes

None. This adds a new workflow; the Docker Hub workflows are unchanged.

Testing

Build, signing and all three verifications were proven green on the PR branch (runs 36905293807, 36905967976, 36908671913) while it still had a temporary branch trigger publishing :test; that trigger is removed, so only release tags are published.

Manual tag build (works before merge, from the branch):

gh workflow run docker-ghcr.yml --ref test/ghcr-private -f tag=2.3.0

When the workflow is run manually from a branch, the cosign identity is that branch's workflow ref (e.g. ...docker-ghcr.yml@refs/heads/develop), not the tag's.

Tag triggers only fire when the workflow file is in the tagged commit, so they can be checked only after merge:

  1. Push a tag such as 2.4.1-beta from develop.
  2. Confirm the run publishes ghcr.io/grandnode/grandnode2:2.4.1-beta.
  3. Verify the image locally:
    cosign verify ghcr.io/grandnode/grandnode2:2.4.1-beta \
      --certificate-oidc-issuer https://token.actions.githubusercontent.com \
      --certificate-identity https://github.com/grandnode/grandnode2/.github/workflows/docker-ghcr.yml@refs/tags/2.4.1-beta
    gh attestation verify oci://ghcr.io/grandnode/grandnode2:2.4.1-beta --repo grandnode/grandnode2
    

Updated GitHub Actions workflow to use newer versions of actions and added image signing step.
Updated the GitHub Actions workflow to include SBOM generation and verification steps.
Updated GitHub Actions workflow for Docker release. Changed trigger from branch to tag, updated action versions, and modified steps for versioning and SBOM verification.
Added workflow_dispatch input for Docker image version and removed unnecessary steps for building and pushing Docker images.
Removed unnecessary YAML code block delimiters from the workflow file.
Updated Docker image version description and validation regex for manual builds.
Updated GitHub Actions workflow to trigger on push to the 'test/ghcr-private' branch and removed commented-out sections for clarity.
Removed commented-out sections and simplified version determination steps.
Updated workflow to reflect GHCR naming and paths.
BuildKit attestations are unsigned, so cosign verify-attestation found
none. Generate the SBOM with Syft and attest it with cosign, attest SLSA
provenance with attest-build-provenance, verify against the exact
workflow identity, pin actions to SHAs and scope permissions to the job.
Tags like 2.3.0 and 2.4.0-beta publish an image named after the tag;
the test branch keeps :test. Pass GIT_COMMIT/GIT_BRANCH build args like
the Docker Hub workflow, and skip the org storage record that needs
artifact-metadata: write.
Copilot AI balanced review requested due to automatic review settings October 1, 2026 18:33

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.

Copilot review overview

🟡 Changes recommended

SLSA verification does not enforce the claimed exact workflow ref and uses a vulnerable GitHub CLI verification path.

Review effort: Balanced
Findings: 1 High severity

Open (1)
What changed in this PR

Adds a hardened GHCR release workflow for signed GrandNode images with SBOM and SLSA provenance.

Changes:

  • Builds and publishes release-tagged GHCR images.
  • Generates, signs, and verifies SBOM and provenance attestations.
  • Pins actions and restricts workflow permissions.
File Description
.github/​workflows/​docker-ghcr.yml Defines the complete GHCR release and verification pipeline.

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

Comment thread .github/workflows/docker-ghcr.yml
workflow_dispatch takes an optional tag input; the job checks out that
tag, validates its format and labels the image with the commit actually
built, so tags created before this workflow existed can be published.
Drop the test/ghcr-private branch trigger and the :test fallback; a run
without a release tag now fails instead of publishing.
--signer-workflow omitted the ref and, in gh versions affected by
GHSA-wjmr-j3rp-mh2g, was matched as a prefix. Use --cert-identity with
the same workflow_ref identity as the cosign checks, and reject
attestations from self-hosted runners.
@KrzysztofPajak
KrzysztofPajak merged commit 80f41fa into develop Oct 2, 2026
6 checks passed
@KrzysztofPajak
KrzysztofPajak deleted the test/ghcr-private branch October 2, 2026 05:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants