← All guides

Artifact provenance: proving where a build came from

Build & artifacts
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 scanningwhat is in this artifact that is known to be vulnerable
SBOMwhat is in this artifact at all
Provenancedid 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 byVerified against
A GitHub Actions workflowthe workflow's own URL, at the ref it ran on
A GitLab CI jobthe project URL and config file
A certificate from your CAthe 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:latest

It 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 controlA 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 failedAn 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 elseAn 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 provenance is for.
  • actions/attest defaults push-to-registry to 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:latest verifies 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.
ToolNotes
cosignOSS, from Sigstore; signs and verifies keyless, and what Draugr runs for them
notationOSS, the Notary Project CLI; signatures that chain to roots you hold
SLSA / in-totoThe frameworks describing what a build should attest to, beyond who signed it

Keep learning