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.
Build provenance
Section titled “Build provenance”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: trueWhat 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:
gh attestation verify oci://ghcr.io/OWNER/REPO:1.4.2 --repo OWNER/REPOThis 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.
Software bills of materials
Section titled “Software bills of materials”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: trueSPDX 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.
Dependency review on pull requests
Section titled “Dependency review on pull requests”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.0It 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.
Signing
Section titled “Signing”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:
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_PLACEHOLDERWithout the identity constraint, verification proves only that somebody signed it — and anyone can sign anything.
Hardening the build itself
Section titled “Hardening the build itself”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.sumverification — 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.
An end-to-end chain
Section titled “An end-to-end chain”-
Pull request — dependency review reports new dependencies; CI runs with
permissions: {}. -
Merge — the build job produces an image and outputs its digest.
-
Attest — provenance and SBOM attestations are generated for that digest and pushed to the registry.
-
Deploy — the deployment job verifies the attestation, then deploys by digest, behind an environment with required reviewers.
-
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.
Exercise
Section titled “Exercise”-
Add
actions/attest-build-provenance@v4to a container build. Confirm the attestation appears on the repository’s Attestations page. -
Verify it with
gh attestation verify. Then verify with the wrong--repoand confirm it fails. -
Generate an SBOM and attest it. Retrieve it from the registry rather than from an artifact.
-
Add a verification step to the deployment workflow before the deploy, and confirm the pipeline fails when verification fails.
-
Add dependency review to pull requests and open one adding a dependency with a known high-severity advisory. Confirm it is blocked.
-
Sign with cosign, then verify twice — once with
--certificate-identity-regexpand once without — and note that the second proves much less.
Then what?
Section titled “Then what?”That completes the security cluster and the GitHub Actions pillar.