Skip to content

GitHub Artifact Attestations Explained

Lesson 4 of 10Advanced15 min readGit Security & DevSecOps · Supply Chain SecurityVerified: actions/attest v4, gh attestation verify, GitHub artifact attestations documentation, September 2026

Pillar 4 introduced artifact attestations as a workflow capability: add a step, get provenance.

This page is about what that provenance claims, why the claim is trustworthy, and what a consumer does with it. The generation is two lines. Everything that makes it worth having is on the verification side.

An attestation is a signed statement about an artifact, generated by GitHub during the workflow that produced it.

The subject is the artifact, identified by its digest — not by its name or tag.

The claim is the provenance: which repository, which commit, which workflow, which event, which runner.

The signature comes from Sigstore, using a short-lived certificate bound to the workflow’s OIDC identity. No key is stored anywhere.

Verification is gh attestation verify, and it can assert not just that an attestation exists but that it names the repository and workflow you expect.

Why the identity matters more than the signature

Section titled “Why the identity matters more than the signature”

The interesting part of this mechanism is not cryptographic. It is whose identity is doing the signing and how it came to exist.

When a workflow requests id-token: write, GitHub issues an OIDC token containing claims about the run: the repository, the workflow file, the ref, the event, the commit SHA. Those claims are asserted by GitHub, and a workflow cannot alter what its own token says about it.

The attestation flow uses that token to obtain a short-lived signing certificate from Sigstore’s certificate authority, binding the certificate to those claims. The artifact’s provenance is signed with the corresponding key, and the key is discarded.

Two consequences that make this different from ordinary signing:

There is no key to steal. Nothing long-lived exists. A compromise of your repository does not yield a signing key, because there is not one.

The signature carries an identity you can assert on. Verification is not “is this signed?” but “is this signed by the identity corresponding to that workflow in that repository?” — which is a much stronger question, and it is the whole reason the mechanism is worth using.

Sigstore covers the machinery.

For a binary or file:

permissions:
id-token: write
contents: read
attestations: write
steps:
- uses: actions/checkout@v7
- name: Build
run: ./build.sh
- name: Generate artifact attestation
uses: actions/attest@v4
with:
subject-path: dist/my-binary

For a container image, referenced by digest and pushed to the registry alongside the image:

permissions:
id-token: write
contents: read
attestations: write
packages: write
steps:
- id: build
uses: docker/build-push-action@v7
with:
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Generate artifact attestation
uses: actions/attest@v4
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true

Three details worth understanding rather than copying.

id-token: write is what lets the job request an OIDC token. Without it there is no identity, and nothing to bind a certificate to.

The digest comes from the build step’s output. Not from a tag. A tag can move between the push and the attestation; the digest cannot. This is the single most important line in the container example.

push-to-registry: true stores the attestation in the registry next to the image, so anyone pulling the image can find it without querying GitHub. For an artifact consumed outside your organisation, this is what makes verification practical.

Generation is preparation. Verification is the control.

Terminal window
{/* A binary, verified against the repository that should have built it */}
gh attestation verify PATH/TO/BINARY -R YOUR_ORG/YOUR_REPO
Terminal window
{/* A container image note the oci:// prefix and the registry login */}
docker login ghcr.io
gh attestation verify oci://ghcr.io/YOUR_ORG/YOUR_IMAGE:TAG -R YOUR_ORG/YOUR_REPO

The -R flag is doing the essential work. Without it, you are asking “does an attestation exist for these bytes?”, which almost anything can satisfy. With it, you are asking “was this built from the repository I expect?”

Tighter still, asserting the specific workflow:

Terminal window
gh attestation verify oci://ghcr.io/YOUR_ORG/YOUR_IMAGE:TAG \
--repo YOUR_ORG/YOUR_REPO \
--signer-workflow YOUR_ORG/YOUR_REPO/.github/workflows/release.yml

That last constraint is what a real policy asserts. An attacker who can run any workflow in your repository can produce an attestation; one who must produce an attestation naming your release workflow has a much harder problem.

The same mechanism, carrying an inventory rather than provenance:

- uses: actions/attest-sbom@v4
with:
subject-path: dist/my-binary
sbom-path: sbom.spdx.json

The value over publishing an SBOM as a file is the binding. A file next to a binary has no verifiable relationship to it; an SBOM attestation is cryptographically tied to the artifact’s digest and signed by the build’s identity.

An artifact can carry both — a provenance attestation saying where it came from and an SBOM attestation saying what is in it. They are separate statements about the same subject, and consumers usually want both. See SBOMs.

When an attestation is pushed to a registry with push-to-registry: true and the workflow has artifact-metadata: write, GitHub records the artifact on a linked artifacts page showing build history, deployment records and storage details.

The operational value is connecting an artifact in a registry back to its source, its build run and its owning team — which is the question that is hard to answer at 03:00 when a vulnerability is reported against an image whose provenance nobody recorded.

This is one of the places where supply-chain evidence pays off in ordinary operations rather than only in a compromise: “which team owns this image and where was it built” is asked far more often than “was this image tampered with”.

Verification prints a summary, and it is worth knowing what is inside rather than treating the command as a pass/fail oracle.

An attestation is a signed in-toto statement: a subject, a predicate type, and a predicate. For provenance, the predicate is a SLSA provenance document describing the build.

The fields that matter when you read one:

FieldWhat it tells you
subjectThe artifact’s name and digest — what this statement is about
predicateTypeWhich kind of claim this is: provenance, SBOM, something else
builder.idThe identity that performed the build
buildDefinitionThe workflow, the ref, the event that triggered it
resolvedDependenciesThe top-level inputs, including the source repository and commit
runDetailsMetadata about the run itself

Two of those are the ones a policy asserts on. builder.id answers “who built this” — and for GitHub-hosted builds it names GitHub rather than your workflow, which is why --signer-workflow exists as a separate constraint on the certificate’s identity. resolvedDependencies names the source repository and commit, which is the link back to code somebody reviewed.

Inspecting one directly:

Terminal window
{/* Fetch the attestations GitHub holds for an artifact by digest */}
gh api /repos/YOUR_ORG/YOUR_REPO/attestations/sha256:YOUR_DIGEST --jq '.attestations[].bundle.dsseEnvelope.payload' | base64 -d | jq .

Doing this once, on a real artifact, converts the mechanism from abstract to concrete faster than any amount of reading — and it shows you exactly which fields your policy could be asserting on and is not.

Two practical constraints that shape where attestations are useful.

Verification needs access to the attestation. For an artifact built in a private repository, the attestation lives with the repository, and a consumer who cannot read the repository cannot fetch it through the API. push-to-registry: true is what makes verification possible for anyone who can pull the image, because the attestation travels with the artifact rather than living behind repository access.

Verification needs a way to name what is expected. A consumer outside your organisation verifying your image needs to know which repository and workflow to assert. That is documentation you publish — a line in your README saying “release images are built by .github/workflows/release.yml in YOUR_ORG/YOUR_REPO; verify with…”. Without it, a consumer can verify that an attestation exists and not that it is the right one.

Publishing that expectation is the part producers skip, and it is what turns your attestations from a capability into something a consumer can act on.

The gap between having attestations and being protected by them is a policy that can refuse something.

- name: Verify the image before deploying
run: |
gh attestation verify "oci://${IMAGE}" \
--repo "${GITHUB_REPOSITORY}" \
--signer-workflow "${GITHUB_REPOSITORY}/.github/workflows/release.yml"
env:
IMAGE: ghcr.io/${{ github.repository }}@${{ inputs.digest }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The step fails if verification fails, and a failed step stops the deployment. That is the whole mechanism — no continue-on-error, no warning.

Stronger, because it applies to everything reaching the cluster regardless of which pipeline sent it. A Kubernetes admission controller verifying attestations rejects any image whose provenance does not match policy, including one pushed by somebody bypassing your deployment workflow entirely.

That is the difference worth understanding: a check in your pipeline verifies the path you control. A check at admission verifies everything that arrives.

Threat. An artifact that was not produced by your build reaches production, or an artifact of yours is modified after the build.

Attack surface. Everything between the build and the deployment: the registry, the artifact store, the deployment configuration that selects which artifact runs, and anything that can push under your names.

Impact. Whatever the substituted artifact does, running with your production access, distributed by your own channel.

Control. Bind each artifact to its build with a signed attestation, and refuse to deploy anything whose attestation does not match an expected repository, workflow and ref.

Verification. Take an artifact from your registry and verify it from a machine that had nothing to do with building it, asserting the workflow you expect. Then take an artifact that should fail — one built by a different workflow — and confirm the check rejects it.

That second half is the verification of the verification, and it is the one that catches a policy with a typo in the workflow path, or a check whose failure mode is a warning. A control you have never seen say “no” is a control you have not tested.

Retention, and the questions that come later

Section titled “Retention, and the questions that come later”

Attestations are evidence, and evidence is only useful while it exists.

How long does the attestation outlive the artifact? A container image that stays in a registry for three years needs its attestation to be available for three years. Where the attestation is pushed to the registry, it is subject to the same retention policy as the image, which is usually what you want. Where it lives only with the repository, it is subject to whatever GitHub retains and to the repository continuing to exist.

What happens when the repository is renamed or archived? The provenance names a repository. A rename does not invalidate the signature, and it does change what a policy asserting the old name matches. Worth checking before a reorganisation, and worth remembering when a verification suddenly starts failing for no apparent reason.

Can you answer questions about a two-year-old release? “Which commit produced the binary our customer is running?” is the question attestations exist to answer, and it is usually asked long after the build logs have rotated. If retention on the attestation is shorter than the artifact’s supported life, the evidence expires before the question arrives.

None of this is exotic. It is the ordinary observation that a record has a lifetime, and that the lifetime should be at least as long as the thing it describes.

Publishing an immutable release automatically generates a release attestation — a cryptographically verifiable record of the release containing the release tag, the commit SHA and the release assets.

That is a different subject from a build provenance attestation, and the difference is worth being precise about.

A build provenance attestation is about one artifact: these bytes were produced by this workflow from this commit.

A release attestation is about the release: this tag, pointing at this commit, published these assets. It binds the whole set together, which is the claim a consumer downloading from a releases page actually needs — they are not verifying a binary they built, they are verifying that this is the release the project published.

The two compose. A release whose assets each carry build provenance, and which itself carries a release attestation, lets a consumer establish both that each asset came from a known build and that the set is the published release rather than an assembled imitation.

Both produce signed statements about artifacts using Sigstore. The difference is where the identity comes from and how much you operate.

GitHub artifact attestationsCosign directly
IdentityThe workflow’s OIDC identity, automaticallyWhatever OIDC identity you configure, or a key you manage
SetupA permissions block and one stepInstall the tool, configure signing, handle storage
StorageGitHub, and optionally the registryWherever you push it
Verificationgh attestation verifycosign verify / verify-attestation
Works outside GitHub ActionsNoYes
Custom predicatesLimited to what the actions supportAny predicate you construct

Use artifact attestations when you build in GitHub Actions and want provenance with minimal operation. Which is most cases, and the reason it exists.

Use Cosign directly when you build elsewhere, need a predicate type the actions do not produce, or are signing artifacts that are not build outputs — a configuration bundle, a machine learning model, a dataset.

They interoperate, because both produce Sigstore-signed in-toto statements. An image attested by GitHub can be verified with cosign verify-attestation given the right identity constraints, which matters when your verification happens in a Kubernetes admission controller rather than in a workflow. See Signing container images.

Whether the artifact is vulnerable. Provenance is origin. Scanning is a separate activity with a separate answer.

Whether the source was any good. It names the commit. It has no opinion about what the commit contains.

Whether the build was correct. A build that compiles the wrong thing produces accurate provenance about having done so.

Whether the workflow was trustworthy. If an attacker modified the release workflow and it was merged, the attestation faithfully records that the modified workflow built the artifact — and it verifies.

That last one deserves emphasis, because it defines the boundary. Attestations bind an artifact to a build. They do not vouch for the build’s contents. The controls that make the build trustworthy are repository protection, pinned actions and least-privilege permissions. Attestation is what makes the resulting chain checkable, not what makes it sound.

The steps are small and the ordering is what makes them add up to something.

  1. Add attestation generation to one build, for one artifact. Two lines plus a permissions block. Confirm an attestation appears.

  2. Verify it manually, from your laptop, with --repo. This is the step that teaches you what the command actually asserts, and it takes a minute.

  3. Publish what consumers should expect — the repository and workflow — somewhere they will find it. For an internal artifact, that is the runbook; for a public one, the README.

  4. Add verification to the deployment, failing on mismatch. This is the first point at which anything is protected.

  5. Tighten the assertion to name the workflow, then the ref. Each tightening should be tested by confirming that something which ought to fail does.

  6. Move verification to admission, if you deploy to a platform that supports it, so the check covers everything arriving rather than everything your pipeline sends.

  7. Extend to SBOM attestations, once there is somewhere to consume them.

  8. Repeat for the rest of your artifacts, in order of how much damage a substituted one would do.

Step 4 is where the security benefit begins, and steps 1 to 3 are frequently mistaken for the whole project. An organisation that has completed steps 1 to 3 across fifty repositories and step 4 in none has invested in evidence and bought no control.

Attesting by tag rather than digest. The tag can move between the push and the attestation, producing a valid attestation for the wrong bytes.

Verifying without --repo. Asks “is this signed?” rather than “is this from where I expect?”

Deriving the expected repository from the artifact. The artifact vouching for itself.

Verification that warns instead of failing. A step that reports and continues is documentation.

Generating attestations and never verifying anything. The common outcome, and it delivers a compliance artefact rather than a control.

Believing attestation implies safety. It attests to origin. Vulnerabilities are separate.

Forgetting id-token: write. No OIDC token, no identity, no attestation — and the failure looks like a generic permissions bug rather than a missing capability.

Verifying only in your own pipeline. Covers the path you control, and nothing that bypasses it.

Letting the attestation’s retention expire before the artifact’s. The evidence has to outlive the thing it describes, or the question arrives after the answer is gone.

One repository is an afternoon. Fifty is a different problem, and the failure mode is fifty slightly different implementations.

Put generation in a reusable workflow. One definition of the build-and-attest steps, called by every repository. This is also what the higher SLSA build levels require, so it is work that counts twice.

Put verification in a composite action or reusable workflow too. A verification step copied fifty times will diverge, and the divergence will be in the direction of not failing.

Define the expected identity centrally. The policy — which repositories and workflows are acceptable builders — belongs in one place, not in each deployment workflow’s environment variables.

Measure coverage, not adoption. The useful number is not “how many repositories generate attestations” but “what proportion of deployments verified one, and how many verifications have ever failed”. A verification that has never failed in six months is either a very well-run pipeline or a check that cannot fail, and telling those apart requires deliberately breaking it once.

An attestation is a signed sentence: “this build system, running this workflow, from this commit, produced exactly these bytes.” Verification checks the sentence against what you expected. Whether those bytes are any good is a different sentence that nobody has written.

  • An attestation binds a claim to an artifact’s digest, signed via the workflow’s OIDC identity
  • No signing key is stored, so there is no key for a repository compromise to yield
  • The digest must come from the build step’s output, never from a tag
  • push-to-registry: true stores the attestation beside the image so consumers can find it
  • gh attestation verify with --repo and --signer-workflow is what makes verification a policy
  • Verifying that “an attestation exists” is close to no check at all
  • SBOM attestations bind an inventory to the same subject, and are a separate statement
  • Linked artifacts connect a registry image back to its build, team and deployments
  • Admission-time verification covers everything arriving; pipeline verification covers your path only
  • Attestations bind artifact to build; they do not vouch for what the build did

Use a disposable public repository.

  1. Build a small binary in a workflow and add actions/attest@v4. Predict: what happens if you omit id-token: write?

  2. Download the artifact and run gh attestation verify without -R. Predict: does it pass, and what has it actually established?

  3. Run it with -R naming the correct repository, then with a different one. Compare.

  4. Add --signer-workflow naming the workflow. Then rename the workflow file and re-verify. Predict: does the old attestation still verify against the new name?

  5. Build a container image and attest by digest with push-to-registry: true. Verify with the oci:// form.

  6. Now attest using the tag rather than the digest. Push a different image to the same tag and re-verify. Predict: what does the attestation now describe?

  7. Add a deployment job that verifies before deploying, and confirm that a failed verification stops it — not warns.

  8. Delete the repository.

GitHub Actions Security ChecklistToken permissions, fork pull requests, script injection and supply chain — with the attack each item prevents.

The repository security templates — secrets management and least-privilege token guides — are in the Professional Toolkit.