GitHub Artifact Attestations: Verify Build Provenance Before You Deploy

A checksum tells you whether the bytes changed. It does not tell you which workflow built those bytes, which commit was used, or whether the build came from the repository you expected. That gap matters when a release binary lands in a deployment script or a container image moves from CI into a cluster.

GitHub artifact attestations are a practical way to answer that provenance question for GitHub Actions builds. They are not a magic safety stamp, and GitHub’s own artifact attestations overview warns that attestations are not a guarantee that an artifact is secure. Used well, they give you signed evidence that you can verify before you run, package, or promote an artifact.

If you already check hashes, keep doing it. I wrote about what SHA-256 checksums prove, and this is the next layer: binding a digest to a build identity and policy.

On this page
  1. What GitHub’s attestation actually proves
  2. Add provenance after the build step, not before it
  3. Verify the artifact identity, not only the signature
  4. Treat the SLSA predicate as evidence, not policy
  5. Use SBOM attestations only when consumers verify them
  6. Where this still leaves work for your release process

What GitHub’s attestation actually proves#

GitHub describes artifact attestations as cryptographically signed claims that establish build provenance. The official concept page says those claims include a link to the workflow associated with the artifact, plus repository, organization, environment, commit SHA, triggering event, and other information from the GitHub Actions OIDC token.

That is useful because it moves the question from “does this file match a checksum posted beside it?” to “does this file match a signed statement from the build system I trust?” The difference is subtle but large. A checksum copied into the same compromised release note as the binary proves little. A signed attestation can be checked against an identity that did not come from the release note.

The actions/attest README says the action binds a named artifact and digest to a predicate using the in-toto attestation format, then signs it with a short-lived Sigstore-issued signing certificate. The same README notes that public repositories use the Sigstore public-good instance, while private or internal repositories use GitHub’s private Sigstore instance, and that GitHub Enterprise Server is not supported for this action.

Add provenance after the build step, not before it#

The simplest safe pattern is to build the release artifact, calculate or reference the final artifact, and only then create the attestation. GitHub’s artifact attestation how-to shows the required workflow permissions for binaries: read-only repository contents, attestation writing, and the GitHub Actions OIDC permission set to write. The OIDC permission is what lets the action request the signing certificate; the attestation permission lets GitHub store the attestation.

name: release
on:
  workflow_dispatch:

permissions:
  contents: read
  attestations: write

# Add the GitHub Actions OIDC permission at write scope here.
# GitHub documents this as the id-token permission.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build release binary
        run: make dist/app-linux-amd64
      - name: Attest release binary
        uses: actions/attest@v4
        with:
          subject-path: dist/app-linux-amd64

That example is intentionally small. In a real release workflow, the attested subject should be the final artifact that users or deployment automation consume. If you rebuild, rename, repackage, or compress the file after the attestation step, you have made the attestation less useful because the verified subject is no longer the thing people run.

For container images, GitHub’s how-to uses subject-name, subject-digest, and push-to-registry: true. It also says the image name should be fully qualified, such as ghcr.io/user/app, and should not include a tag. The digest, not a mutable tag, is the stable subject.

Verify the artifact identity, not only the signature#

The GitHub CLI manual for gh attestation verify is blunt about what verification needs. You provide a local file path or an OCI image URI, and you must scope lookup with either --owner or --repo. By default, the command enforces the https://slsa.dev/provenance/v1 predicate type.

gh attestation verify ./app-linux-amd64 \
  --repo ORG/REPO \
  --signer-workflow github.com/ORG/REPO/.github/workflows/release.yml

The extra signer workflow check is worth adding. The CLI manual says actor identity includes the repository or owner and the Actions workflow that produced the attestation, and it says the workflow path is ideally validated with --signer-workflow or --cert-identity. Without that, you may only know that something in the repository produced an attestation, not that the locked-down release workflow did it.

For an OCI image, the same manual accepts an image URI prefixed with oci://. It also notes that registry authentication must already be in place for private or protected registries.

docker login ghcr.io

gh attestation verify oci://ghcr.io/org/app@sha256:abc123... \
  --repo ORG/REPO \
  --bundle-from-oci

If your team already scans images, provenance should sit beside that work, not replace it. An attestation can tell you where the image came from; a scanner still checks packages and vulnerabilities. The distinction matters in the same way that container scanning answers a different question than release signing.

Treat the SLSA predicate as evidence, not policy#

The SLSA v1.0 provenance specification defines the predicate type GitHub verifies by default: https://slsa.dev/provenance/v1. SLSA says provenance describes how an artifact was produced so consumers can verify the artifact was built according to expectations and, if desired, rebuild it.

The same specification separates trusted and untrusted parts of the build record. In SLSA’s model, externalParameters are values under external control and must be verified downstream. That is a useful warning for policy design. Do not read a provenance JSON blob and assume every field is equally trustworthy or equally meaningful for your risk decision.

The GitHub CLI manual makes the same point in operational terms. With JSON output, only the signature certificate and verified timestamps are described as values that cannot be manipulated by the workflow that originated the attestation. The statement predicate may contain user-controllable metadata. If an attacker can influence the workflow execution context, they may be able to falsify parts of that predicate.

gh attestation verify ./app-linux-amd64 \
  --repo ORG/REPO \
  --signer-workflow github.com/ORG/REPO/.github/workflows/release.yml \
  --format json

That JSON is still valuable. Feed it into a policy check that rejects unexpected repositories, unexpected workflow paths, self-hosted runners if your policy forbids them, wrong source refs, or missing verified timestamps. Just keep the policy narrow and explicit. Provenance is evidence for a decision, not the decision itself.

Use SBOM attestations only when consumers verify them#

GitHub’s how-to also documents SBOM attestations. For a binary, the action can take both subject-path and sbom-path. For an SPDX SBOM, GitHub’s example verifies with --predicate-type https://spdx.dev/Document/v2.3. The actions/attest README says SBOM mode supports SPDX or CycloneDX JSON files and has a 16 MB file-size limit for the SBOM input.

The main trap is creating SBOM attestations that nobody checks. If the consuming side only runs the default provenance verification, the SBOM attestation may exist but have no effect on deployment. Decide which predicates matter, then verify those predicates in the same place you verify the artifact itself.

If you are outside GitHub’s attestation flow, Sigstore’s Cosign verification documentation covers signature and attestation verification, including keyless verification with OIDC identities and cosign verify-attestation. Do not mix tools blindly: write down whether your verifier trusts GitHub’s attestation API, an OCI registry bundle, a local bundle, or a Cosign signature path.

Where this still leaves work for your release process#

Artifact attestations do not stop a malicious dependency, a careless release approval, or a workflow that builds from the wrong input. They also do not replace secret scanning. If a compromised token can push code, change a workflow, or publish a package, provenance may faithfully record a bad build. Controls like protected branches, minimal workflow permissions, and pre-commit secret checks still matter; the pre-commit secret scanning guide is a better fit for that part of the pipeline.

The practical next step is small: pick one release job that produces a binary or container image, add actions/attest@v4 after the final artifact is created, and add one consumer-side gh attestation verify command that pins the repository and signer workflow. Once that passes, decide which fields your deployment policy actually trusts, and reject everything else by default.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *