Skip to content
Français
CI/CD & Automatisation medium

Verifying GitHub attestations

40 min de lecture

Read this page in French

Generating attestations is only worth something when they are verified. This guide explains how to validate the provenance of your artefacts before deploying them.

What you will learn

  • Verify an artefact with gh attestation verify
  • Block an unverified deployment in a CI/CD workflow
  • Use cosign and slsa-verifier for SLSA verification
  • Show the SLSA level reached through a README badge
  • Enforce verification at admission time in Kubernetes

Why verify?

Verifying an attestation means making sure an artefact really is the one you think. An attestation proves:

  • The artefact comes from the right repository
  • It was built by the right workflow
  • The source code matches the expected commit
  • The signature is valid (nothing was altered)

Without verification, an attacker could substitute a malicious artefact with nothing raising a flag.

With the GitHub CLI

The GitHub CLI (gh) is the most direct way to verify an attestation: it queries the GitHub API and validates the Sigstore signature.

Installation

Install the CLI for your system:

Fenêtre de terminal
# macOS
brew install gh
# Linux (Debian/Ubuntu)
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list
sudo apt update && sudo apt install gh

Verifying a local file

Point the command at the downloaded file and name the expected repository:

Fenêtre de terminal
# Verify a downloaded artefact
gh attestation verify my-app-v1.0.0.tar.gz --owner owner --repo repo

The output on success:

Loaded digest sha256:abc123... for file my-app-v1.0.0.tar.gz
Loaded 1 attestation from GitHub API
✓ Verification succeeded!
SHA256 digest: abc123...
Sigstore Bundle: present
Certificate Subject: https://github.com/owner/repo/.github/workflows/release.yml@refs/tags/v1.0.0
Certificate Issuer: https://token.actions.githubusercontent.com

Verifying a Docker image

For an image, prefix the reference with oci://:

Fenêtre de terminal
# Verify an image from a registry
gh attestation verify oci://ghcr.io/owner/app:v1.0.0 --owner owner

Verification options

Several options tighten the verification: an exact digest, the signing workflow, machine-readable output:

Fenêtre de terminal
# Verify against a specific digest
gh attestation verify --digest sha256:abc123... --owner owner --repo repo
# Verify that the source workflow is the right one
gh attestation verify my-app.tar.gz \
--owner owner \
--repo repo \
--signer-workflow release.yml
# JSON format for automated processing
gh attestation verify my-app.tar.gz --owner owner --format json

In a CI/CD workflow

Verification earns its keep when automated: a deployment must never consume an artefact whose provenance has not been validated.

Verifying before deployment

This workflow downloads an artefact, verifies its attestation, and deploys only if the verification succeeds.

name: Deploy
on:
workflow_dispatch:
inputs:
version:
description: 'Version to deploy'
required: true
permissions: {}
jobs:
deploy:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Download the artefact
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
gh release download "$VERSION" \
--repo "$GITHUB_REPOSITORY" \
--pattern "my-app-*.tar.gz"
- name: Verify the attestation
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh attestation verify my-app-*.tar.gz \
--owner "$GITHUB_REPOSITORY_OWNER" \
--repo "${GITHUB_REPOSITORY#*/}"
- name: Deploy (only if the verification succeeded)
run: ./deploy.sh my-app-*.tar.gz

Verifying an image before pulling it

Same principle for an image: the attestation is validated before the docker pull.

name: Deploy Image
on:
workflow_dispatch:
inputs:
version:
description: 'Version to deploy'
required: true
permissions: {}
jobs:
deploy:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Verify the image attestation
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
gh attestation verify \
"oci://ghcr.io/$GITHUB_REPOSITORY:$VERSION" \
--owner "$GITHUB_REPOSITORY_OWNER"
- name: Pull and deploy
env:
VERSION: ${{ inputs.version }}
run: |
docker pull "ghcr.io/$GITHUB_REPOSITORY:$VERSION"
kubectl set image deployment/app "app=ghcr.io/$GITHUB_REPOSITORY:$VERSION"

With cosign

cosign is the Sigstore signing tool, used behind the scenes by GitHub attestations.

Installation

Install the cosign binary:

Fenêtre de terminal
# macOS
brew install cosign
# Linux
curl -LO https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64
sudo install cosign-linux-amd64 /usr/local/bin/cosign

Verifying an image

cosign verify checks the keyless signature against the identity of the expected workflow:

Fenêtre de terminal
# Verify with the Sigstore certificates
cosign verify \
--certificate-identity-regexp="https://github.com/owner/repo/.github/workflows/*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/owner/app:v1.0.0

Verifying the SLSA attestation

cosign verify-attestation additionally validates the SLSA provenance predicate:

Fenêtre de terminal
# Download and verify the attestation
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity-regexp="https://github.com/owner/repo/.github/workflows/*" \
--certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
ghcr.io/owner/app:v1.0.0

With slsa-verifier

slsa-verifier is the official SLSA tool for verifying provenance attestations.

Installation

Install it through Go or by downloading the binary:

Fenêtre de terminal
# Go install
go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latest
# Or download the binary
curl -LO https://github.com/slsa-framework/slsa-verifier/releases/latest/download/slsa-verifier-linux-amd64
chmod +x slsa-verifier-linux-amd64
sudo mv slsa-verifier-linux-amd64 /usr/local/bin/slsa-verifier

Verifying a local artefact

Download the artefact and its provenance file, then run the verification:

Fenêtre de terminal
# Download the artefact and its attestation from a release
gh release download v1.0.0 --repo owner/repo --pattern "*.tar.gz"
gh release download v1.0.0 --repo owner/repo --pattern "*.intoto.jsonl"
# Verify with slsa-verifier
slsa-verifier verify-artifact my-app-v1.0.0.tar.gz \
--provenance-path my-app-v1.0.0.intoto.jsonl \
--source-uri github.com/owner/repo \
--source-tag v1.0.0

The output on success:

Verified signature against tlog entry index 12345 at URL https://rekor.sigstore.dev
Verified build using builder "https://github.com/actions/runner" at commit abc123def456
SLSA verification passed
✓ Verification succeeded!

Verifying a container image

For an image, prefer verification by digest rather than by tag:

Fenêtre de terminal
# Verify a GHCR image by tag
slsa-verifier verify-image ghcr.io/owner/app:v1.0.0 \
--source-uri github.com/owner/repo \
--source-tag v1.0.0
# Verify by digest (safer)
slsa-verifier verify-image ghcr.io/owner/app@sha256:abc123... \
--source-uri github.com/owner/repo \
--source-tag v1.0.0

Showing the SLSA level reached

The --print-provenance option prints the full predicate, including the buildType that determines the SLSA level:

Fenêtre de terminal
# Verify and print the SLSA level in verbose mode
slsa-verifier verify-image ghcr.io/owner/app:v1.0.0 \
--source-uri github.com/owner/repo \
--source-tag v1.0.0 \
--print-provenance

The detailed output:

{
"_type": "https://in-toto.io/Statement/v1",
"subject": [
{
"name": "ghcr.io/owner/app",
"digest": {"sha256": "abc123..."}
}
],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://slsa-framework.github.io/github-actions-buildtypes/workflow/v1"
}
}
}

The SLSA level:

buildTypeSLSA level
workflow/v1SLSA L3
workflow/v0SLSA L2

A complete verification script

This script chains the verification and the display of the SLSA level, and fails cleanly when something is wrong:

#!/bin/bash
# verify-slsa.sh - Verify and print the SLSA level
set -e
IMAGE="$1"
SOURCE_URI="$2"
SOURCE_TAG="$3"
if [ -z "$IMAGE" ] || [ -z "$SOURCE_URI" ] || [ -z "$SOURCE_TAG" ]; then
echo "Usage: $0 <image> <source-uri> <source-tag>"
echo "Example: $0 ghcr.io/owner/app:v1.0.0 github.com/owner/repo v1.0.0"
exit 1
fi
echo "SLSA verification of $IMAGE"
echo ""
# Verify with slsa-verifier
if slsa-verifier verify-image "$IMAGE" \
--source-uri "$SOURCE_URI" \
--source-tag "$SOURCE_TAG" \
--print-provenance > /tmp/slsa-provenance.json 2>&1; then
echo "SLSA verification succeeded"
echo ""
# Extract the SLSA level
BUILD_TYPE=$(jq -r '.predicate.buildDefinition.buildType' /tmp/slsa-provenance.json)
echo "SLSA level reached:"
if [[ "$BUILD_TYPE" == *"workflow/v1"* ]]; then
echo " SLSA Level 3"
elif [[ "$BUILD_TYPE" == *"workflow/v0"* ]]; then
echo " SLSA Level 2"
else
echo " Unknown level: $BUILD_TYPE"
fi
echo ""
echo "Provenance details:"
jq '{repository: .predicate.buildDefinition.externalParameters.workflow.repository, ref: .predicate.buildDefinition.externalParameters.workflow.ref, builder: .predicate.runDetails.builder.id}' /tmp/slsa-provenance.json
else
echo "SLSA verification failed"
exit 1
fi

Usage:

Fenêtre de terminal
chmod +x verify-slsa.sh
./verify-slsa.sh ghcr.io/stephrobert/test-sigstore:v1.0.2 github.com/stephrobert/test-sigstore v1.0.2

Showing the SLSA badge in the README

To communicate the SLSA level reached, add a badge to your README.md.

A static badge

The static badge reflects the level you target, to be updated as the project progresses:

<!-- SLSA Level 3 badge -->
[![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev)
<!-- SLSA Level 2 badge -->
[![SLSA 2](https://slsa.dev/images/gh-badge-level2.svg)](https://slsa.dev)
<!-- SLSA Level 1 badge -->
[![SLSA 1](https://slsa.dev/images/gh-badge-level1.svg)](https://slsa.dev)

Rendered:

SLSA 3

A dynamic badge with OpenSSF Scorecard

This badge updates automatically with the repository's real score:

[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/owner/repo/badge)](https://scorecard.dev/viewer/?uri=github.com/owner/repo)

It shows the overall score (which includes SLSA among other criteria).

A continuous verification badge

If you have an automatic verification workflow, its status badge proves the verification really runs:

[![SLSA Verification](https://github.com/owner/repo/actions/workflows/verify-slsa.yml/badge.svg)](https://github.com/owner/repo/actions/workflows/verify-slsa.yml)

A complete README example

Here is how those badges and the security section fit together in a README:

# My Application
[![SLSA 3](https://slsa.dev/images/gh-badge-level3.svg)](https://slsa.dev)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/owner/repo/badge)](https://scorecard.dev/viewer/?uri=github.com/owner/repo)
[![Build](https://github.com/owner/repo/actions/workflows/release.yml/badge.svg)](https://github.com/owner/repo/actions/workflows/release.yml)
## Supply Chain Security
This project reaches **SLSA Level 3** to guarantee the traceability of the supply chain:
- SLSA provenance generated for every release
- Sigstore signatures on every image
- Attestations verifiable with `gh attestation verify`
### Verifying a release
```bash
# Verify the Docker image
gh attestation verify oci://ghcr.io/owner/repo:v1.0.0 --owner owner
# With slsa-verifier
slsa-verifier verify-image ghcr.io/owner/repo:v1.0.0 \
--source-uri github.com/owner/repo \
--source-tag v1.0.0
```

An automatic verification workflow

Create .github/workflows/verify-slsa.yml to verify your releases automatically, every night:

name: SLSA Verification
on:
schedule:
# Verify every night
- cron: '0 2 * * *'
workflow_dispatch:
permissions: {}
jobs:
verify:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Install slsa-verifier
run: |
curl -LO https://github.com/slsa-framework/slsa-verifier/releases/latest/download/slsa-verifier-linux-amd64
chmod +x slsa-verifier-linux-amd64
sudo mv slsa-verifier-linux-amd64 /usr/local/bin/slsa-verifier
- name: Get the latest release
id: latest
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
LATEST_TAG=$(gh release view --repo "$GITHUB_REPOSITORY" --json tagName -q .tagName)
echo "tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
- name: Verify the SLSA provenance
env:
RELEASE_TAG: ${{ steps.latest.outputs.tag }}
run: |
slsa-verifier verify-image "ghcr.io/$GITHUB_REPOSITORY:$RELEASE_TAG" \
--source-uri "github.com/$GITHUB_REPOSITORY" \
--source-tag "$RELEASE_TAG" \
--print-provenance
- name: Report the status
if: always()
env:
JOB_STATUS: ${{ job.status }}
RELEASE_TAG: ${{ steps.latest.outputs.tag }}
run: |
if [ "$JOB_STATUS" = "success" ]; then
echo "SLSA verification succeeded for $RELEASE_TAG"
else
echo "SLSA verification failed for $RELEASE_TAG"
exit 1
fi

This workflow:

  1. Runs every night at 2am
  2. Fetches the latest release
  3. Verifies its SLSA attestation
  4. Fails if the verification does not pass

You can then show that workflow's badge in your README.

Automating it in Kubernetes

Verification can be enforced at admission: the cluster refuses any pod whose image has no valid attestation.

With Kyverno

Kyverno applies a policy requiring a GitHub keyless attestation on the images:

apiVersion: policies.kyverno.io/v1
kind: ImageValidatingPolicy
metadata:
name: verify-attestation
spec:
validationActions: [Deny]
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
matchImageReferences:
- glob: "ghcr.io/owner/*"
attestors:
- name: github-keyless
cosign:
keyless:
identities:
- issuer: "https://token.actions.githubusercontent.com"
subjectRegExp: "^https://github.com/owner/"
validations:
- expression: "true"
message: "Image must have a valid SLSA provenance attestation."

With the Sigstore Policy Controller

The Sigstore Policy Controller offers a ClusterImagePolicy with an equivalent role:

apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: github-attestation
spec:
images:
- glob: "ghcr.io/owner/**"
authorities:
- keyless:
identities:
- issuer: "https://token.actions.githubusercontent.com"
subjectRegExp: "https://github.com/owner/.*"

Common errors

Three failures come back often; each has a precise cause and a fix.

"No attestations found"

Error: no attestations found for digest sha256:abc123

Possible causes:

  1. The artefact has no attestation generated
  2. The digest does not match
  3. The repository has not enabled attestations

"Verification failed"

Error: verification failed: signature mismatch

Possible causes:

  1. The artefact was modified after signing
  2. The wrong file is being verified
  3. The attestation belongs to another artefact

"Certificate identity mismatch"

Error: certificate identity mismatch

Cause: the workflow that signed does not match the expected criteria.

Fix: check --signer-workflow or --certificate-identity-regexp.

Verification checklist

Before accepting a verification, go through this list; each point closes a way around it:

  • Check that the SHA256 digest matches the artefact
  • Check the source repository (--owner, --repo)
  • Check the source workflow when it is critical (--signer-workflow)
  • Check the expected version or tag
  • Automate the verification in the deployment pipeline

Key points

  • An unverified attestation protects nothing: verification is the step that closes the loop.
  • gh attestation verify is the most direct route; cosign and slsa-verifier cover the advanced SLSA needs.
  • In CI/CD, verify before deploying: no unvalidated artefact should reach production.
  • The --print-provenance option reveals the buildType, and therefore the SLSA level actually reached.
  • In Kubernetes, Kyverno or the Sigstore Policy Controller enforce verification at admission.

For the full reference, see the documentation of gh attestation verify and the slsa-verifier repository.

Next steps

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn