Provenance Attestation
A build provenance attestation is a signed statement describing how an artifact was built: the source commit, the builder, and the build parameters.
A build provenance attestation is a signed statement describing how an artifact was built: the source commit it came from, the builder that ran the build, and the build parameters that run was given. The statement names its artifact by cryptographic digest, so anyone holding a container image or a release binary can fetch the matching attestation and check where the bytes came from before trusting them.
An image tag, a release filename, and a green check on a pull request all say nothing verifiable about origin. A provenance attestation moves that question from convention to signature, because the record was produced by the build system itself and signed with an identity a verifier can pin.
Definition
A provenance attestation has three parts: a subject, a predicate, and a signature wrapping both.
The subject names the artifact by content hash. Naming by digest matters because a tag can be moved onto different content later, while a digest resolves to the exact bytes it was computed from. Bind the statement to the tag and the record drifts away from the artifact; bind it to the digest and the two stay together.
The predicate carries the facts. The in-toto attestation framework defines the outer envelope shape, and a predicateType URI declares which vocabulary the body uses (in-toto attestation statement spec, checked on 2026-08-13). The common vocabulary for build provenance is SLSA Provenance, published at https://slsa.dev/provenance/v1 (SLSA provenance specification, checked on 2026-08-13).
SLSA Provenance splits its body in two, and the split is the useful mental model for the whole term:
| Section | Field | What it records |
|---|---|---|
buildDefinition | buildType | A URI naming the schema of the parameters below, so a verifier knows how to read them |
buildDefinition | externalParameters | The inputs a person controlled: the repository, the ref, the workflow file path, dispatch inputs |
buildDefinition | internalParameters | Values the build platform chose on its own |
buildDefinition | resolvedDependencies | The source commit and other inputs pinned to a hash at run time |
runDetails | builder.id | A URI identifying the system that executed the build |
runDetails | metadata.invocationId | The specific run, usually a URL to the log |
runDetails | metadata.startedOn and finishedOn | Timestamps for the run |
The signature is what turns a description into evidence. Attestations are commonly signed with a short-lived certificate issued to the build's workload identity rather than a long-lived key held by a person, and the issuance is written to a public transparency log so the record can be audited after the certificate expires. The workload identity comes from an OIDC token the build platform mints for the running job, which is why the signer's certificate can carry the repository, the workflow, and the ref as identity fields instead of a human name.
What verification actually checks
Verification is a sequence of independent checks, and each one catches a different substitution:
| Check | Failure it catches |
|---|---|
| The signature verifies against the certificate in the envelope | The statement was edited after signing |
| The certificate identity matches the expected repository and workflow | A valid attestation produced by an unrelated build |
| The subject digest equals the digest of the artifact in hand | An attestation copied from a different build of the same project |
The predicateType is the one the policy requires | An SBOM presented where provenance was required |
| A transparency log entry exists for the signing time | Signing outside the recorded validity window |
Every one of those checks is about the record. None of them inspects the artifact's behavior, so provenance answers the question of origin and leaves the question of quality to tests, scanners, and review.
Where the statement lives
The signed statement is a separate document from the artifact, referenced by digest, and there are three usual homes for it. A registry can store it as a referrer of the image manifest, which the OCI distribution specification supports through the referrers API (OCI distribution spec, checked on 2026-08-13). A build platform can keep it in its own attestation store and serve it by digest lookup. A release process can publish it as a file beside the binary it describes.
Storing it in the registry has one practical advantage: copying the image between registries can carry the attestation along with it, so the evidence survives promotion from a build registry to a production one.
Example
This workflow builds a container image, pushes it, and then attaches a signed provenance attestation bound to the digest the registry returned. actions/attest-build-provenance generates the SLSA statement, signs it with the job's workload identity, and pushes it to the registry as a referrer of the image manifest (GitHub artifact attestations documentation, checked on 2026-08-13).
name: release
on:
push:
tags: ["v*"]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}
- uses: actions/attest-build-provenance@v2
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: trueTwo permissions carry the weight here. id-token: write lets the job request the OIDC token that proves which repository, workflow, and ref it is, and attestations: write lets it publish the resulting statement. Without the first, the signer has no identity to bind the certificate to.
The statement that comes out looks like the excerpt below, trimmed to the fields a reader cares about:
{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "ghcr.io/acme/api",
"digest": { "sha256": "9f2c...c41d" }
}
],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1",
"externalParameters": {
"workflow": {
"ref": "refs/tags/v1.4.0",
"repository": "https://github.com/acme/api",
"path": ".github/workflows/release.yml"
}
},
"resolvedDependencies": [
{
"uri": "git+https://github.com/acme/api@refs/tags/v1.4.0",
"digest": { "gitCommit": "c0ffee1234567890abcdef1234567890abcdef12" }
}
]
},
"runDetails": {
"builder": {
"id": "https://github.com/actions/runner/github-hosted"
},
"metadata": {
"invocationId": "https://github.com/acme/api/actions/runs/7412/attempts/1"
}
}
}
}Read it from the bottom up and it answers the origin question directly: run 7412 of release.yml, triggered on tag v1.4.0, from commit c0ffee1, executed by a hosted runner, producing the manifest with digest sha256:9f2c...c41d. A build executed on machines you supply reports a different builder.id, so the builder field distinguishes hosted execution from a fleet you registered yourself.
A consumer verifies the image before deploying it:
gh attestation verify oci://ghcr.io/acme/api@sha256:9f2c...c41d --repo acme/apiThat command resolves the digest, fetches the attestations attached to it, checks the signature and the transparency log entry, and fails unless the signer identity belongs to acme/api. Pointing it at an image built from a fork, rebuilt on a laptop, or pushed by a compromised token fails at the identity check rather than at the signature check, which is the distinction worth understanding: a valid signature from the wrong identity is still a rejection.
Image builders can also emit provenance without a separate signing step. BuildKit attaches a SLSA provenance attestation to the image index when a build is run with provenance enabled, and mode=max records the full set of build arguments and resolved dependencies rather than a minimal summary (Docker SLSA provenance attestation documentation, checked on 2026-08-13):
- uses: docker/build-push-action@v6
with:
context: .
push: true
provenance: mode=max
sbom: true
tags: ghcr.io/${{ github.repository }}:${{ github.ref_name }}The two paths answer different questions and are frequently used together. The builder-generated attestation describes the build graph in detail. The signed attestation from the workflow ties that artifact to a repository identity a policy engine can pin at deploy time.
Related Terms
- How an SBOM lists the contents of an artifact: the companion document that records what went into a build rather than how the build ran.
- Attaching build provenance to an image in GitHub Actions: the permissions, the action inputs, and the verification command in one worked sequence.
- OIDC token exchange and the workload identity a signer proves: where the short-lived identity in the signing certificate comes from.
- WarpBuild Docker builders documentation: how a workflow points an image build at a remote builder.
- WarpBuild pricing: per minute rates by runner type.
FAQ
What is the difference between provenance and an SBOM?
Provenance describes how an artifact was produced: the source repository and commit, the builder identity, the workflow file, and the parameters that run was given. An SBOM describes what is inside the artifact: the packages and versions assembled into it. They are separate documents with separate predicate types, and a build can publish both against the same digest.
Does a provenance attestation prove the build is safe?
No. The statement records what the build system observed, and verification answers who signed it and whether the subject digest matches the artifact in hand. Whether the recorded source, builder, and parameters are acceptable is a policy decision the verifier makes after the signature checks out.
Where is the attestation stored if it is not inside the image?
The signed statement is a separate document referenced by the artifact digest. It can be pushed to the same OCI registry as a referrer of the image manifest, kept in the build platform's own attestation store and fetched by digest at verification time, or written next to a released binary as a file.
Start with $10 in free credits
Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.