Skip to content
RJJ Software Jamie Taylor · fractional CTOBook a call
Menu

Writing · 5 December 2025

Attestations: The Missing Link Between Your Code and Production Trust

Build provenance attestations prove how your software was built. Why I added them to OwaspHeaders.Core, and how to adopt them in four tested levels.

The image shows two virtual panes of glass passing over each other, similar to Apple's Liquid Glass UI language. Both panes of glass have the names of a number of open-source fonts and different design-related words. Where the glass objects cross over there is some displacement of the text, giving a feeling of depth to the image.

ℹ️ Rewritten 17 September 2026

I first published this post on 5 December 2025, and I have since rewritten it. Two quotes I attributed to guests of The Modern .NET Show did not appear in the episodes I cited, and a third overstated what was said; all three have been replaced with the guests’ actual words. The code examples have been corrected and tested, and an illustrative client rollout has been removed.

On 23 December 2024, I added build provenance attestations to the release pipeline for OwaspHeaders.Core, the security headers middleware for ASP.NET Core that I maintain. Version 9.5.0 went out the same day, and every one of the 17 releases from 9.5.0 to 10.4.0 carries signed, publicly verifiable evidence of the repository, workflow, and commit that built it; I checked each of them while rewriting this post.

I had several reasons for doing it then. US federal agencies had started collecting secure software development attestations from their software suppliers that year. Thirteen days earlier, the EU Cyber Resilience Act had come into force, and I had been discussing it with clients; it is far easier to be ready before regulations apply than to scramble to adopt a practice after they do. A package that exists to make other people’s applications more secure should also offer its consumers proof of how it was built, for anyone who wants to check. And two conversations on The Modern .NET Show that year, with Niels Tanis and Adrian Mouat, had changed how I thought about the software supply chain. Neither regulation required build attestations; I was getting ahead of them.

In my earlier post on Software Bills of Materials (SBOMs), I explained how an SBOM records what is in your software. It can’t prove that the thing running in production was built from the code you reviewed, using the dependencies your SBOM lists, by the process you approved. Attestations can. An attestation is a cryptographically signed statement about an artefact, such as “this package was built from this commit, by this workflow, on this date”, which anyone with the right public key or identity can verify, and which breaks the moment the artefact changes.

What an SBOM Can’t Tell You

An SBOM generated from source misses one kind of attack entirely. A developer commits clean code, and it passes review. Somewhere between that commit and the release, the build system is compromised, whether by an outside attacker or an insider, and malicious code is injected during the build. The resulting binary is deployed to production. If your SBOM was generated from the source tree, it still shows the dependencies you expected, because the source tree was never touched. The first sign of trouble arrives months later, as a breach.

In the SolarWinds compromise, attackers deployed a malicious tool into the build environment that injected a backdoor into the Orion platform as it was being built. The source code the developers worked on was clean, and the product their customers installed was signed. Niels Tanis made the point in our discussion on The Modern .NET Show about hidden dependencies:

SolarWind still remains the biggest example of their product being compromised at build stages, right? It’s hard. They even had a package that was signed.

A signature tells you who published a file. It says nothing about how that file was built, which is the question an attestation answers. Without one, you cannot prove that the injection step didn’t happen; with one, a binary that doesn’t match its signed provenance fails verification before it gets anywhere near production.

What Attestations Actually Prove

Attestations are structured, signed claims about your software supply chain, and different kinds of attestation answer different questions. Adrian Mouat described the idea well in our conversation about Chainguard and container security:

You can create an attestation that says, “hey, I ran a scanner on this image and I had this output at this time.” And because it’s all signed, you know that that did happen… So it’s sort of proving that certain steps were taken.

Build Provenance

Build provenance is the foundation, and the one I would start with. It records which source commit was built, which build system and identity built it, when the build ran, and the environment and parameters it ran with. Most tools emit it in the Supply-chain Levels for Software Artifacts (SLSA) provenance format, wrapped in an in-toto statement, so a consumer can check it with standard tooling.

Dependency Verification

An SBOM attestation binds your bill of materials to a specific artefact digest, and the signature is evidence that this list describes this build. It lets a consumer check that each dependency matches its declared version and came from the expected source, and, where your dependencies publish attestations of their own, it lets that chain of trust extend further down the stack.

Security Scanning Results

A vulnerability attestation records that a scan was performed against a specific artefact, by a specific scanner, at a specific time, along with what it found. An attestation full of findings is still a valid attestation: it is evidence that you looked, and a record of what you knew when you shipped, which is useful both for audits and for the uncomfortable conversation after a new vulnerability is disclosed.

Test Execution

The same pattern extends to testing: signed evidence that the required test suites ran against this build and passed, that coverage met your threshold, or that a policy gate was evaluated. A pipeline rule that refuses to merge without passing tests is a promise; an attestation that the tests ran is the receipt.

Attestations have limits. An attestation proves that a particular identity signed a particular claim; it does not prove the claim is true. If the build platform itself is compromised, it can sign convincing provenance for a malicious build, which is why SLSA’s higher build levels concentrate on hardening the build platform itself. Attestations move the question from “do we trust this binary?” to “do we trust the system that built it?”, which is a much smaller and much more auditable thing to trust.

What I Still Find in Build Pipelines

When I review build and release processes, the same picture comes up often enough that I have stopped being surprised by it, and I still see it today in some organisations. The good news is that most of the foundations are in place. Source control has audit logs. Continuous integration and continuous delivery (CI/CD) pipelines have access controls. Security scanning runs somewhere in the pipeline, and some teams produce SBOMs.

What is almost always missing is anything that connects those pieces. There is no cryptographic link between the commit that was reviewed and the binary that was deployed, and so no way to show that the build wasn’t tampered with. Artefacts move from build to registry to deployment without a chain of custody, and nothing verifies what is actually running at deploy time. Each control does its job, but the evidence each one produces stays inside its own tool; when someone asks “is this the code that passed review?”, the honest answer is a collection of logs and a lot of trust, and attestations are what turn that collection into evidence.

Implementing Attestations in Four Levels

You don’t need to adopt everything at once. This is the order I recommend: each level is useful on its own, and each builds on the one before it. The examples below were tested in September 2026 against cosign v3.1.3, Syft v1.51.1, Grype v0.118.0, and Open Policy Agent (OPA) v1.20.2.

Level 1: Basic Build Attestations

Start by attesting your build outputs. On GitHub Actions, this is a single step plus the permissions it needs:

# GitHub Actions example
permissions:
  id-token: write
  attestations: write
  artifact-metadata: write

steps:
  # ...build steps...
  - uses: actions/attest@v4
    with:
      subject-path: ./build/app.exe

This creates a signed SLSA provenance attestation recording which repository built the artefact, which workflow ran, which commit triggered it, and when. The overhead is small: in the two most recent OwaspHeaders.Core releases for which GitHub still holds step timings, the attestation step took two seconds.

Anyone can then verify an artefact with the GitHub CLI. If you publish to NuGet, verifying a package downloaded from nuget.org fails in a way that looks like a broken attestation. nuget.org adds its own repository signature to every package it hosts, as a .signature.p7s entry inside the .nupkg, and that changes the file’s digest. The package you download is not byte-for-byte the package your pipeline attested, so gh attestation verify reports that no attestation exists. Removing the signature entry restores the original bytes, a workaround Andrew Lock documented in 2025:

# Download OwaspHeaders.Core 10.4.0 from nuget.org
curl -sLO https://api.nuget.org/v3-flatcontainer/owaspheaders.core/10.4.0/owaspheaders.core.10.4.0.nupkg

# Remove the signature nuget.org adds, restoring the bytes that were attested
zip -d owaspheaders.core.10.4.0.nupkg .signature.p7s

# Verify the build provenance
gh attestation verify owaspheaders.core.10.4.0.nupkg --repo GaProgMan/OwaspHeaders.Core

When I ran this, verification confirmed the package was built by the release.yml workflow on main, on a GitHub-hosted runner, from a specific commit; a copy with a single byte appended failed.

Level 2: SBOM Attestations

Next, link your SBOM to your build:

# Generate the SBOM from the built image, not the source tree
syft scan myapp:latest -o cyclonedx-json > sbom.json

# Create attestation linking SBOM to artefact
cosign attest --predicate sbom.json --type cyclonedx \
  --key cosign.key myapp:latest

Generating the SBOM from the built image rather than the source tree matters, for the reason covered earlier: an SBOM of the source cannot see what the build added. Now you can prove your SBOM matches your artefact.

By default, cosign records each signing event in Rekor, Sigstore’s public transparency log. The entry holds hashes, the signature, and your public key or signing certificate, and the image digest can be searched; the SBOM itself is not stored. So the log reveals that you attested an image, not what you attested, and with keyless signing it also reveals the signing identity. If that matters for your private images, run your own Sigstore instance. GitHub’s attestation action makes this choice for you: public repositories use the public Sigstore instance, and private or internal repositories use GitHub’s own.

Level 3: Vulnerability Attestations

Add security scan results:

# Scan for vulnerabilities
grype sbom:./sbom.json -o json > vulns.json

# Attest the results. Grype's report isn't in the format cosign's
# built-in "vuln" type expects, so give it a predicate type of its own
# (use a URI your organisation controls)
cosign attest --predicate vulns.json \
  --type https://example.com/attestations/vulnerability-report/v1 \
  --key cosign.key myapp:latest

This proves security scans were performed and what they found. Cosign’s built-in vuln type expects cosign’s own schema; when I gave it Grype’s report instead, it signed and verified an attestation with an empty result. Giving the report its own type keeps the full scan in the signed payload.

Level 4: Policy Attestations

Finally, implement policy-as-code attestations. Start with a policy, here written in Rego for OPA:

# policies.rego - policy: no critical vulnerabilities
package release

critical := [m | some m in input.matches; m.vulnerability.severity == "Critical"]

decision := {
	"allow": count(critical) == 0,
	"critical_vulnerabilities": count(critical),
}

Then evaluate it against the scan results and attest the decision:

- name: Evaluate and attest release policy
  run: |
    opa eval --format raw --data policies.rego --input vulns.json \
      'data.release.decision' > policy-results.json
    cosign attest --predicate policy-results.json \
      --type https://example.com/attestations/policy-decision/v1 \
      --key cosign.key myapp:latest

Against a test image built on an old Alpine release, this produced {"allow":false,"critical_vulnerabilities":5}, signed and attached to the image. The attestation records the decision; enforcing it is the job of whatever verifies attestations at deploy time.

The Tool Ecosystem

The .NET world has been having a version of this conversation too. Scott Harden and I talked about publishing packages in a conversation about modern NuGet packaging on The Modern .NET Show, where he said the steps we covered make it “a little bit easier to signal that you are who you say you [are] and the package contains what it should contain”. Those are the same two questions an attestation answers, and these are the tools I would look at for answering them:

ToolWhat it doesCreates or verifies
Sigstore and cosignSigning and attestation for container images and other artefacts, with a public transparency logBoth
GitHub artifact attestationsactions/attest in workflows, gh attestation verify for consumersBoth
SLSAA specification with levels for build and source integrity, and the provenance format most tools emitNeither; it is a specification
in-totoThe statement format that SLSA provenance and GitHub attestations are wrapped inNeither; it is the format
Open Policy AgentGeneral-purpose policy engine; Gatekeeper is its Kubernetes admission controllerVerifies, through policy
KyvernoKubernetes policy engine whose verifyImages rules check signatures and in-toto attestations at admissionVerifies
Google Cloud Binary AuthorizationAttestation-based deploy-time policy for GKE, Cloud Run, and Cloud Service MeshVerifies

AWS takes a different approach. AWS Signer signs container images using the Notary Project’s Notation tooling, and Amazon EKS checks those signatures through third-party admission controllers such as Gatekeeper with Ratify, or Kyverno. That verifies who signed an image rather than attestations about how it was built, so on AWS, attestation verification means adding one of the open-source tools above.

Where the Regulation Stands

When I first published this post, I described attestations as becoming mandatory. The EU picture is still moving that way, on a timeline worth having in your diary. The Cyber Resilience Act entered into force on 10 December 2024; its reporting obligations have applied since 11 September 2026, and its main obligations apply from 11 December 2027. The Act does not name attestations specifically, though I expect signed evidence of how your software was built, and what it contains, to make its obligations easier to meet. In the US, I also pointed to Executive Order 14028, and that picture has changed.

ℹ️ Updated 17 September 2026

The US requirement has changed since this post was published. On 23 January 2026, the Office of Management and Budget withdrew memo M-22-18, which had required federal agencies to collect a secure software development attestation from their software suppliers. Agencies can still ask for one, or for an SBOM, but they no longer have to. That attestation was also a signed form about development practices, not the cryptographic build attestations this post describes.

Where to Start

When supply chain attacks can target the build rather than the code, “trust me” isn’t a security strategy, and the January 2026 reputation hijacking attempt on NuGet went after package owners directly. Pick one application, ideally one that other teams or customers depend on, and add a Level 1 build attestation this week. Verify it yourself with gh attestation verify, the way a consumer would, so you have seen both sides of the process. Then share the result with your security team, and decide together whether SBOM and vulnerability attestations are worth the next step. If you haven’t produced an SBOM yet, my SBOM guide is the place to begin.

If you would like help building a verifiable software supply chain, from the first attestation through to deploy-time policy, let’s talk and we can work out where your pipeline stands.

More from writing

Next step

Bring me the decision you keep deferring

A discovery call costs nothing and commits you to nothing. You'll leave with an honest read on your situation and a clear next step, whether or not that step involves me.

Book a discovery call