Introducing Tenki's code reviewer: deep, context-aware reviews that actually find bugs.Try it for Free
Overview of GitHub Actions artifact attestations, SLSA provenance, and supply chain security defaults
Hayssem Vazquez-Elsayed
Hayssem Vazquez-ElsayedFeb 27, 2026
GitHub ActionsSecuritySupply Chain SecuritySLSASBOM

GitHub Actions Artifact Attestations: SLSA Provenance and Supply Chain Defaults


TL;DR

GitHub's artifact attestation feature uses Sigstore to generate SLSA provenance for your builds. Here's how it works, what it proves, and what changes as defaults tighten.

Every time you run a GitHub Actions workflow that produces a binary, a container image, or a package, there's a question downstream consumers can't easily answer: did this artifact actually come from the repository and workflow it claims to come from? Code signing with long-lived keys has been the traditional answer, but it creates its own problems. Keys get leaked, rotated poorly, or stored in CI secrets that are one misconfiguration away from exposure.

GitHub's artifact attestation feature takes a different approach. Built on Sigstore, it generates cryptographically signed provenance statements that tie an artifact to a specific workflow run, commit, and repository. No long-lived keys. No key management overhead. The signing happens with ephemeral certificates that live for ten minutes, and the proof lands on an immutable transparency log.

This isn't new, exactly. Artifact attestations went GA in June 2024. But what's changing is the default posture. GitHub has been steadily tightening enforcement through 2025 and into 2026, and for public repositories, attestation generation is shifting from opt-in to default behavior. If your workflows produce release artifacts and you haven't thought about attestations yet, you're about to encounter them whether you planned to or not.

What attestations prove (and what they don't)

An artifact attestation is a signed statement that says: this artifact, identified by its SHA-256 digest, was produced by this specific GitHub Actions workflow run, in this repository, from this commit. The statement follows the in-toto attestation format and includes a predicate containing the build provenance metadata: the workflow file, the runner environment, the triggering event, and the OIDC token claims from the Actions runtime.

This is different from traditional code signing in a few important ways. Code signing answers the question "who vouches for this artifact?" Attestations answer "where and how was this artifact built?" A signed binary tells you that someone with a particular key blessed it. An attestation tells you the exact workflow, commit, and environment that produced it. You can verify both independently.

What attestations don't prove: that the source code is safe, that the dependencies are free of vulnerabilities, or that the build process itself is hardened. Provenance is a paper trail, not a security audit. It lets you trace an artifact back to its origin, which is the foundation for policy decisions, but it doesn't make those decisions for you.

How Sigstore powers the signing pipeline

The machinery under the hood involves three Sigstore components working together: Fulcio (certificate authority), a timestamp authority, and Rekor (transparency log). Here's the actual flow when your workflow runs actions/attest-build-provenance:

  1. The attestation action requests the GitHub Actions OIDC token for the current workflow run. This token contains claims about the repository, workflow, commit SHA, and runner environment.
  2. A fresh keypair is generated in memory. The public key and OIDC token are sent to Fulcio, which validates the token and issues a short-lived X.509 certificate (valid for 10 minutes) binding the public key to the workflow's identity.
  3. The client computes the SHA-256 digest of the artifact and writes an in-toto statement that binds the artifact hash to a SLSA provenance predicate containing the OIDC claims.
  4. The private key signs the statement inside a DSSE (Dead Simple Signing Envelope). The timestamp authority counter-signs to prove the signature was created within the certificate's validity window. Then the private key is discarded.
  5. Everything is bundled into a Sigstore bundle and stored in GitHub's attestation store. For public repositories, the bundle is also written to the Sigstore Public Good Instance's Rekor log, making it publicly auditable.

For private repositories, the flow is identical except GitHub's own internal Fulcio instance issues the certificate, and nothing touches public logs. The attestation stays in GitHub's private store. No customer data leaks to external infrastructure.

GitHub established itself as a root certificate authority in October 2023 to make this work. The trust root is managed through a quorum of hardware tokens held by employees in different geographies and roles, using TUF (The Update Framework) tooling. Intermediate certificates sit in Azure KeyVault Managed HSMs. Because signing certificates are only valid for 10 minutes, certificate revocation lists aren't needed.

Adding attestations to your workflow

The implementation is six lines of YAML. For a binary artifact, you need the right permissions and a call to the attestation action after your build step:

permissions:
  id-token: write
  contents: read
  attestations: write

steps:
  - name: Build
    run: make build

  - name: Generate artifact attestation
    uses: actions/attest-build-provenance@v3
    with:
      subject-path: 'dist/my-binary'

Three permissions matter here. id-token: write lets the action request the OIDC token from the Actions runtime. attestations: write grants access to GitHub's attestation store. contents: read is needed for checkout. If you forget any of these, the action fails with an unhelpful permissions error.

The subject-path parameter accepts glob patterns, so you can attest multiple files at once. The action computes the digest of each file and creates attestations for all of them.

Attesting container images pushed to GHCR

Container images need slightly different handling. Instead of a file path, you provide the fully-qualified image name and its SHA-256 digest. You also need packages: write permission so the attestation can be pushed to the registry alongside the image.

permissions:
  id-token: write
  contents: read
  attestations: write
  packages: write

steps:
  - name: Build and push image
    id: push
    uses: docker/build-push-action@v6
    with:
      push: true
      tags: ghcr.io/my-org/my-app:latest

  - name: Generate artifact attestation
    uses: actions/attest-build-provenance@v3
    with:
      subject-name: ghcr.io/my-org/my-app
      subject-digest: ${{ steps.push.outputs.digest }}
      push-to-registry: true

Two things to watch here. First, subject-name must not include a tag. Just the registry, org, and image name. Second, the digest comes from the docker/build-push-action output. This is the immutable identifier for the image. Tags are mutable and can be re-pointed; digests can't. The attestation binds to the digest, which is why verification stays reliable even if someone retags the image.

Setting push-to-registry: true stores the attestation in the OCI registry alongside the image itself, making it discoverable by tools that pull from the registry. This also works with registries other than GHCR, like Azure Container Registry.

SBOM attestations and dependency tracking

Build provenance tells you where an artifact came from. An SBOM attestation tells you what's inside it. GitHub provides a separate action for this: actions/attest-sbom@v2. It takes an SBOM file you've already generated and wraps it in a signed in-toto attestation, binding it to a specific artifact.

The action doesn't generate the SBOM itself. You need a separate tool for that. Anchore's Syft is the most common choice and supports both SPDX and CycloneDX formats across most language ecosystems. A typical setup scans the build directory or container image, outputs an SPDX JSON file, then feeds that into the attestation action:

- uses: anchore/sbom-action@v0
  with:
    path: ./build/
    format: 'spdx-json'
    output-file: 'sbom.spdx.json'

- uses: actions/attest-sbom@v2
  with:
    subject-path: 'dist/my-binary'
    sbom-path: 'sbom.spdx.json'

The signed SBOM attestation gets stored in the same attestation store as provenance records. Downstream consumers can then verify it using the GitHub CLI with a specific predicate type flag. For regulatory compliance, like NIST's SSDF or the EU Cyber Resilience Act, having a signed and verifiable SBOM tied to a specific build artifact closes a gap that many organizations currently fill with ad-hoc spreadsheets or unverified SBOM files dropped into release notes.

Verifying attestations on the consumer side

Producing attestations is only half the story. The security benefit comes from verification. The GitHub CLI (version 2.49.0+) handles this with gh attestation verify.

For a binary:

gh attestation verify ./my-binary -R my-org/my-repo

For a container image, prefix the image reference with oci://:

docker login ghcr.io
gh attestation verify oci://ghcr.io/my-org/my-app:v1.2.0 -R my-org/my-repo

Verifying SBOM attestations requires an extra flag to specify the predicate type, since the default predicate is build provenance:

gh attestation verify ./my-binary \
  -R my-org/my-repo \
  --predicate-type https://spdx.dev/Document/v2.3

Add --format json to get machine-readable output you can pipe into policy engines. This is where attestations become genuinely useful in automated pipelines. You can feed the JSON into OPA (Open Policy Agent) or Cue to enforce rules like "only deploy artifacts built from the main branch" or "only accept images built by workflows in approved repositories."

For Kubernetes environments, GitHub provides a Sigstore Policy Controller that can enforce attestation verification as an admission controller. You install it via Helm, configure a trust root pointing to GitHub's certificate authority, and label the namespaces where enforcement should apply. Any image without a valid attestation from your organization gets rejected at admission time.

SLSA levels: where GitHub attestations land

SLSA (Supply-chain Levels for Software Artifacts) defines four build levels, from L0 (no guarantees) through L3 (hardened builds). GitHub's artifact attestations achieve SLSA v1.0 Build Level 2 out of the box.

What does that mean concretely? Build L2 requires that builds run on a hosted platform (not a developer's laptop) and that provenance is signed and tied to that platform's identity. GitHub Actions meets both requirements. The hosted runners provide the infrastructure, the OIDC token provides the identity binding, and Sigstore provides the signature.

Build L3 requires stronger isolation: the build platform must prevent runs from influencing each other, and the signing key material must be inaccessible to user-defined build steps. Standard GitHub Actions workflows don't meet this because the build steps and the attestation step run in the same job, meaning a malicious build step could theoretically interfere.

To reach Build L3, GitHub recommends using reusable workflows. When the build runs inside a reusable workflow called by your repository's workflow, the build logic is isolated from the caller. The reusable workflow controls the build environment and the attestation step, so the calling workflow can't tamper with either. This provides the isolation that L3 demands.

For most teams, L2 is a solid starting point. It stops the most common attack vectors: someone swapping out a binary after the build, or uploading a release artifact that wasn't actually produced by CI. L3 matters more when you're distributing software to environments with strict compliance requirements or when you need to protect against compromised repository credentials.

What changes when attestations become default

GitHub has been moving toward making attestation generation automatic for public repositories. The direction is clear from the trajectory: artifact attestations launched in beta in May 2024, went GA a month later, and through 2025 GitHub added enforcement features including the Kubernetes admission controller, linked artifacts pages with build history, and organization-level attestation policies.

When attestation generation becomes the default for public repos, a few things change for maintainers:

  • Workflows that don't set attestation permissions will get them implicitly. If your workflow already uses a restrictive permissions block, you might need to add attestations: write explicitly to keep things working correctly.
  • Attestation data appears in the Rekor transparency log. For public repos, attestations are written to the public Sigstore log. This means metadata about your build (repository name, workflow file, commit) becomes publicly auditable. That's the point, but it's worth knowing if you weren't expecting it.
  • Downstream consumers will start expecting attestations. Once attestation generation is common, verification becomes the natural next step. Organizations that deploy software from public repositories will start requiring attestation verification in their pipelines and admission controllers. If your project doesn't produce attestations, it'll stick out.
  • Compliance reporting gets easier. For teams dealing with FedRAMP, SOC 2, or similar frameworks, having automated, cryptographically signed provenance records for every release artifact simplifies the evidence collection process considerably.

Practical recommendations

If you're starting from scratch, here's a reasonable approach:

  1. Add build provenance attestations to your release workflows first. Don't attest every CI run. Focus on workflows that produce artifacts you ship to users or deploy to production.
  2. Add SBOM generation and attestation to the same workflow. The marginal effort is small since you're already running the attestation infrastructure, and it provides a real compliance benefit.
  3. Set up verification in your deployment pipeline. Run gh attestation verify before deploying. Start in audit mode (log failures but don't block) and switch to enforcement once you're confident the attestation pipeline is reliable.
  4. Evaluate reusable workflows for L3. If your organization has centralized build templates already, converting them to reusable workflows gets you Build L3 with minimal refactoring.
  5. Pin your attestation action references. Use full commit SHAs, not version tags, for the attestation actions. This is a supply chain security feature, after all. Pinning to @v3 is fine for readability, but your security-critical workflows should reference the immutable SHA.

The underlying point is that attestations aren't a checkbox feature you enable and forget. They're useful exactly to the degree that someone on the other end is actually verifying them. If you're producing software that other teams or organizations consume, the question isn't whether to generate attestations. It's whether you'll adopt the workflow on your own timeline or scramble to add it when a consumer or auditor requires it.

Related News


Get Tenki

Faster Builds. Smarter Reviews. Start Both For Free.

Change 1 line of YAML for faster runners. Install a GitHub App for AI code reviews. No credit card, no contract. Takes about 2 minutes.