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.
The short answer
Section titled “The short answer”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.
Generating
Section titled “Generating”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-binaryFor 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: trueThree 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.
Verifying
Section titled “Verifying”Generation is preparation. Verification is the control.
{/* A binary, verified against the repository that should have built it */}gh attestation verify PATH/TO/BINARY -R YOUR_ORG/YOUR_REPO{/* A container image — note the oci:// prefix and the registry login */}docker login ghcr.iogh attestation verify oci://ghcr.io/YOUR_ORG/YOUR_IMAGE:TAG -R YOUR_ORG/YOUR_REPOThe -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:
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.ymlThat 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.
SBOM attestations
Section titled “SBOM attestations”The same mechanism, carrying an inventory rather than provenance:
- uses: actions/attest-sbom@v4 with: subject-path: dist/my-binary sbom-path: sbom.spdx.jsonThe 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.
Linked artifacts
Section titled “Linked artifacts”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”.
Reading an attestation
Section titled “Reading an attestation”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:
| Field | What it tells you |
|---|---|
subject | The artifact’s name and digest — what this statement is about |
predicateType | Which kind of claim this is: provenance, SBOM, something else |
builder.id | The identity that performed the build |
buildDefinition | The workflow, the ref, the event that triggered it |
resolvedDependencies | The top-level inputs, including the source repository and commit |
runDetails | Metadata 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:
{/* 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.
Private repositories and consumers
Section titled “Private repositories and consumers”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.
Turning verification into policy
Section titled “Turning verification into policy”The gap between having attestations and being protected by them is a policy that can refuse something.
In a deployment workflow
Section titled “In a deployment workflow” - 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.
At admission
Section titled “At admission”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.
The threat model
Section titled “The threat model”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.
Release attestations
Section titled “Release attestations”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.
Attestations or Cosign?
Section titled “Attestations or Cosign?”Both produce signed statements about artifacts using Sigstore. The difference is where the identity comes from and how much you operate.
| GitHub artifact attestations | Cosign directly | |
|---|---|---|
| Identity | The workflow’s OIDC identity, automatically | Whatever OIDC identity you configure, or a key you manage |
| Setup | A permissions block and one step | Install the tool, configure signing, handle storage |
| Storage | GitHub, and optionally the registry | Wherever you push it |
| Verification | gh attestation verify | cosign verify / verify-attestation |
| Works outside GitHub Actions | No | Yes |
| Custom predicates | Limited to what the actions support | Any 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.
What it does not tell you
Section titled “What it does not tell you”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.
Rolling it out
Section titled “Rolling it out”The steps are small and the ordering is what makes them add up to something.
-
Add attestation generation to one build, for one artifact. Two lines plus a permissions block. Confirm an attestation appears.
-
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. -
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.
-
Add verification to the deployment, failing on mismatch. This is the first point at which anything is protected.
-
Tighten the assertion to name the workflow, then the ref. Each tightening should be tested by confirming that something which ought to fail does.
-
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.
-
Extend to SBOM attestations, once there is somewhere to consume them.
-
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.
Common mistakes
Section titled “Common mistakes”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.
Operating this at organisation scale
Section titled “Operating this at organisation scale”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.
Mental model
Section titled “Mental model”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.
What you learned
Section titled “What you learned”- 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: truestores the attestation beside the image so consumers can find itgh attestation verifywith--repoand--signer-workflowis 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
Exercise
Section titled “Exercise”Use a disposable public repository.
-
Build a small binary in a workflow and add
actions/attest@v4. Predict: what happens if you omitid-token: write? -
Download the artifact and run
gh attestation verifywithout-R. Predict: does it pass, and what has it actually established? -
Run it with
-Rnaming the correct repository, then with a different one. Compare. -
Add
--signer-workflownaming the workflow. Then rename the workflow file and re-verify. Predict: does the old attestation still verify against the new name? -
Build a container image and attest by digest with
push-to-registry: true. Verify with theoci://form. -
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?
-
Add a deployment job that verifies before deploying, and confirm that a failed verification stops it — not warns.
-
Delete the repository.