Skip to content

Build Provenance, SBOMs and Artifact Signing in GitHub Actions

Lesson 10 of 10Advanced4 min readGitHub Actions & CI/CD · Actions SecurityVerified: actions/attest-build-provenance v4, actions/attest-sbom v4, actions/dependency-review-action v5, sigstore/cosign-installer v4, August 2026

Everything else in this cluster reduces the chance that your pipeline is subverted. This page is about the question that follows: how would anyone know? If the chain-of-custody idea is new, Software Supply Chain Security Explained sets out the threats at each stage; this page is the GitHub Actions implementation of the evidence.

A deployed artifact is a file. Nothing about it inherently says which commit produced it, which workflow built it, or whether it was replaced in transit. Attestation makes those claims checkable rather than assumed.

permissions:
contents: read
packages: write
id-token: write
attestations: write
steps:
- uses: actions/attest-build-provenance@v4
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true

What it doesGenerates a signed statement binding an artifact's digest to the workflow, repository, commit and runner that produced it.

Why we run itWithout it, 'this image came from our pipeline' is an assumption based on where the file was found. With it, it is a signed claim anyone can verify against GitHub's transparency log.

Expected resultAn attestation visible on the repository's Attestations page and verifiable with the gh CLI.

Four permissions, each doing something distinct: id-token: write obtains the workflow identity that signs the statement, attestations: write stores it, packages: write pushes it alongside the image, and contents: read is the checkout.

The subject is a digest, not a tag. Attesting a mutable tag would produce a claim about whatever that tag points at later, which is no claim at all — the same reason deployment uses digests.

Verification:

Terminal window
gh attestation verify oci://ghcr.io/OWNER/REPO:1.4.2 --repo OWNER/REPO

This checks the signature, confirms the artifact’s digest matches the attested subject, and confirms the attestation came from the repository you named. It answers “was this built by that repository’s workflow” — which is the question you actually care about when pulling an image.

An SBOM lists what is inside an artifact. Its value is entirely in what you can do with it later: when a vulnerability is announced in a library, an SBOM turns “does anything we run include this?” from an archaeology project into a query.

- uses: anchore/sbom-action@v0
with:
image: ghcr.io/${{ github.repository }}@${{ steps.build.outputs.digest }}
format: spdx-json
output-file: sbom.spdx.json
- uses: actions/attest-sbom@v4
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
sbom-path: sbom.spdx.json
push-to-registry: true

SPDX and CycloneDX are the two mainstream formats; both are widely supported and the choice usually follows whatever consumes them downstream.

Attaching the SBOM to the image rather than storing it as an artifact matters, because artifacts expire. An SBOM you can no longer retrieve for a version still running in production is not doing its job.

The cheapest supply-chain control catches problems before they merge:

on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
review:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/dependency-review-action@v5
with:
fail-on-severity: high
deny-licenses: GPL-3.0, AGPL-3.0

It compares the dependency manifests between base and head and reports what a pull request adds — which is the moment a questionable dependency is cheapest to reject.

Availability depends on the repository: it works on public repositories, and on private ones where the dependency graph is enabled, which on some plans requires GitHub Advanced Security. Check Settings → Code security rather than assuming.

Provenance attestations cover the common case. Signing with Sigstore is useful when consumers outside GitHub need to verify, or when you want a signature independent of GitHub’s attestation store:

- uses: sigstore/cosign-installer@v4
- name: Sign the image
run: |
cosign sign --yes "ghcr.io/${GITHUB_REPOSITORY}@${DIGEST}"
env:
DIGEST: ${{ steps.build.outputs.digest }}

Keyless signing uses the workflow’s OIDC identity, so there is no private key to store or rotate — the same architecture as cloud OIDC, applied to signing. The signature is recorded in a public transparency log.

Verification pins who signed it, not merely that it is signed:

Terminal window
cosign verify \
--certificate-identity-regexp "^https://github.com/OWNER/REPO/.github/workflows/release.yml@refs/tags/v" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/OWNER/REPO@sha256:DIGEST_PLACEHOLDER

Without the identity constraint, verification proves only that somebody signed it — and anyone can sign anything.

Attestation records what the build did. These reduce what it can do:

  • Pin every action to a SHA — see pinning actions.
  • Scope permissions per job, so the job running dependencies is not the job that can publish.
  • Use npm ci, pip install --require-hashes, go.sum verification — a build that resolves dependencies freshly cannot make a reproducible claim about what it included.
  • Do not build publishable artifacts from untrusted pull requests, which is the rule from Docker CI.
  • Deploy by digest, so what was attested and what runs are provably the same bytes.
  1. Pull request — dependency review reports new dependencies; CI runs with permissions: {}.

  2. Merge — the build job produces an image and outputs its digest.

  3. Attest — provenance and SBOM attestations are generated for that digest and pushed to the registry.

  4. Deploy — the deployment job verifies the attestation, then deploys by digest, behind an environment with required reviewers.

  5. Incident — when a CVE lands, the SBOMs answer which versions are affected, and the provenance answers which commit built each one.

Step 4 is the one that turns the rest from paperwork into a control.

  1. Add actions/attest-build-provenance@v4 to a container build. Confirm the attestation appears on the repository’s Attestations page.

  2. Verify it with gh attestation verify. Then verify with the wrong --repo and confirm it fails.

  3. Generate an SBOM and attest it. Retrieve it from the registry rather than from an artifact.

  4. Add a verification step to the deployment workflow before the deploy, and confirm the pipeline fails when verification fails.

  5. Add dependency review to pull requests and open one adding a dependency with a known high-severity advisory. Confirm it is blocked.

  6. Sign with cosign, then verify twice — once with --certificate-identity-regexp and once without — and note that the second proves much less.

That completes the security cluster and the GitHub Actions pillar.

GitHub Actions Security ChecklistAudit your workflows against the failure modes that actually cause incidents. Free and complete.

Get the production security checklists and Actions hardening templates from the Professional Toolkit.