Artifact provenance: proving where a build came from
On this page
In plain terms
Artifact provenance is a cryptographic record of where a build came from. A signature is attached to a container image when it is built, naming the workload that built it: this repository, this workflow, this branch. Anyone pulling the image can check that signature and learn who published it.
Scanning tells you what is inside an artifact. Provenance tells you whether the artifact is the one you meant to run.
Why it matters
A registry is a name, not an identity. ghcr.io/acme/payments:v3 is whatever was last pushed
under that tag, by whoever held a token. If a CI credential leaks, an attacker does not need to
find a vulnerability in your code: they push an image, your cluster pulls it on the next restart,
and every scanner you run reports it clean, because there is nothing wrong with it. It is simply
not yours.
This is the gap the last several years of supply-chain attacks went through. The compromised artifact is usually well-formed and passes every check aimed at its contents, because the attack is on the path between your source and your runtime rather than on the source.
A signature closes it. Not by making the image safe, but by making the question answerable: did the workflow I expect produce this?
Where it fits
| Asks | |
|---|---|
| SCA and container scanning | what is in this artifact that is known to be vulnerable |
| SBOM | what is in this artifact at all |
| Provenance | did the builder I expect produce this artifact |
It is deliberately not a substitute for any of them. A signature binds an artifact to a builder, and a builder that has been compromised signs what it is told to. In May 2026 a worm published 84 malicious versions across 42 npm packages, and every one carried a valid SLSA Build Level 3 attestation signed through Sigstore. The attestations were not forged: a real platform built a real commit in a real repository, and the commit was malicious. Provenance narrows who could have done this and never answers is this safe, which is what SCA, SAST and review are for.
How it works
Two ecosystems, two trust models, and the difference decides what you have to store.
Sigstore keyless, the one most projects reach for, issues a short-lived certificate against the identity your CI already has. GitHub Actions, GitLab CI and most hosted runners can request an OIDC token that says which workflow is running; Sigstore's certificate authority exchanges that for a certificate valid for a few minutes, the artifact is signed, and the certificate expires. There is no key to store, rotate or leak, which is the whole point: the identity is the workflow, not a secret somebody holds.
Notary Project signs with a certificate from your own authority, held in a key vault. Azure Pipelines produces these with a key in Azure Key Vault, and an organization with an in-house PKI signs the same way. You hold the roots, so verification needs no external service and nothing contacts a transparency log.
Verification is the mirror image. You state which identity you expect, and the check either matches or does not:
| Signed by | Verified against |
|---|---|
| A GitHub Actions workflow | the workflow's own URL, at the ref it ran on |
| A GitLab CI job | the project URL and config file |
| A certificate from your CA | the roots it must chain to, and the subject it must carry |
How Draugr fits. provenance is a control like any other. It takes signers, each naming the images it covers and
the identity to expect:
config:
controls:
provenance:
enabled: true
signers:
- name: our-ci
images: ["ghcr.io/acme/*"]
github:
repository: acme/ci-workflows
workflow: .github/workflows/build-image.yml
ref: refs/heads/main
An image signed by a workload no signer names is a critical finding, ranked by the exposure and criticality of the component running it. An image no signer covers is reported and not failed, which is where a policy starts rather than where it ends.
Turn it on with no signers at all and nothing fails. The run counts how much of what you already run carries a signature:
$ draugr scan
DRAUGR PASS acme 1.0.0 1.787s
CONTROLS
provenance pass no findings
MEASURED AGAINST
provenance cosign · coverage: 0 of 2 images checked, 1 observed, 1 unsigned · scope: no
signers declared
No findings. ✓observed counts the images signed by somebody you have not named; unsigned the ones carrying
nothing at all. Together they are how much of the inventory a policy would have to cover.
You do not have to copy the identities out by hand. draugr survey provenance reads the
signature on each image you already declare and writes the signers into the descriptor:
$ draugr survey provenance -o draugr.saga.yaml
signers adopted from what signs these images today, not confirmed. Read them before you rely on them:
chainguard-images
https://github.com/chainguard-images/images/.github/workflows/release.yaml@refs/heads/main
read from the signature on cgr.dev/chainguard/static:latestIt writes the exact identity and the exact image, never a pattern, and an image nobody signed produces no signer. Read what it proposes: a signer taken from what signs an image today cannot fail the check it came from, which is what makes it a tripwire for that changing rather than proof that today's answer is right.
| Two trust models, one control | A signer declaring keyless: or github: is verified with cosign; one declaring x509: with notation. Which runs follows from the signer, so a project signing some images each way declares both and passes no flag. |
| Observed is not failed | An image no signer covers is reported and does not fail the gate. unmatched: fail requires full coverage once the inventory has been worked through. |
| Ranked like everything else | An unexpected identity is critical, and lands in the band the component earns from its declared exposure and criticality. |
See the provenance guide.
What a check looks like when it fails
A real run against a real image, expecting a signer it does not carry:
$ draugr scan
DRAUGR FAIL acme 1.4.2 4.362s
1 P1 0 P2 0 P3 0 P4
CONTROLS
provenance FAIL 1 P1
MEASURED AGAINST
provenance cosign · policy: signers: our-ci · pinning: 1 of 1 images verified by tag, not
digest
THE FINDING by priority
Priority Severity Rule Scanner Location
P1 critical provenance-unexpected-identity cosign cgr.dev/chainguard/static:latest
Signed by chainguard-images/images/.github/workflows/release.yaml@refs/heads/main,
not our-ci.The finding names the workflow that did sign, which is the comparison somebody is trying to make. The line above it is the qualifier: this image was named by tag rather than by digest, so what was verified is whatever that tag pointed at during the scan.
Common pitfalls
- A pattern that matches anything accepts anybody.
identityRegexp: .*recognizes every workload on the internet and reads like a configured policy in review. Anchor both ends. - The workflow that signs is not always the one you think. Sigstore derives the identity from
the workflow that actually ran, so a job calling a shared workflow is signed as that workflow's
repository rather than as the caller's. It fails as a mismatch, not as a syntax error. Reading
the identity off the image rather than writing down the one you expect settles it, which is what
draugr survey provenanceis for. actions/attestdefaultspush-to-registryto false. At the default the attestation goes to GitHub's attestations API and nowhere else, so anything reading the registry reports the image as unsigned.- A tag is not an artifact. Verifying
myimage:latestverifies whatever that tag pointed at during the run, and a tag can be moved to other bytes afterwards. A digest cannot. - Starting at
fail. Among the most-downloaded npm packages, the ones publishing with provenance are a small minority, and container images are no further ahead. A control that fails on every third-party image is one somebody switches off in its first week.
Popular & reputable tools
| Tool | Notes |
|---|---|
| cosign | OSS, from Sigstore; signs and verifies keyless, and what Draugr runs for them |
| notation | OSS, the Notary Project CLI; signatures that chain to roots you hold |
| SLSA / in-toto | The frameworks describing what a build should attest to, beyond who signed it |
Keep learning
- Container image scanning, what is inside the artifact whose origin this establishes
- SBOM, the inventory of that artifact
- The security controls landscape, where this sits among the rest
- Risk prioritization, how a provenance finding is ranked against everything else