This article takes a detailed look at GitHub‘s attestation feature, one of several options for creating and verifying attestations for Docker images and files.
Docker Image signing and attestation series
This article is part of a multi-part series:
- Overview: explains concepts and tool landscape, provides high-level recommendations
- Docker Image signing with Notation: detailed analysis of the Notation tool and its signatures
- Docker Image signing and attestation with Cosign: detailed analysis of Cosign, for image signatures and attestations
- (This article) Docker Image attestation with GitHub attestations: detailed analysis of GitHub attestations and its pros&cons
- Docker Image attestations with BuildKit: detailed analysis of BuildKit’s attestations
Introduction
GitHub attestations (docs) allow cryptographic signing (and verification) of artifacts that you build and publish in your GitHub Actions (GHA) workflows. Any attestation type is supported, with dedicated support for build provenance and SBOMs. You can create attestations for Docker images or regular files.
GitHub did not invent its own format for attestations (as Docker did, see the last part of this series), but made the smart move of building upon the existing Sigstore project, that is, GitHub uses Cosign signatures, specifically the new Cosign Bundle Specification. This makes it possible to leverage existing tools for automated verification, such as the Kubernetes operator Kyverno or Sigstore’s Policy Controller.
Heads up if you are unfamiliar with Sigstore / Cosign
To keep this article concise, I assume you are familiar with Cosign signatures. If not, I highly recommend you read my other article first, which explains Cosign signatures in detail.
Why should you create an attestation in your GHA workflow? Because it allows consumers who download your artifacts to manually or automatically verify that the downloaded artifacts have really been built by a GHA workflow of that repo, and were not replaced by a malicious party afterward. As you might know, a bad actor who got their hands on a Personal Access Token (that can manipulate GitHub release artifacts) could simply replace artifacts generated by a GHA workflow with malicious versions (e.g. binaries with a backdoor), or create new tampered releases.
Limitations of GitHub attestations
GitHub’s attestations only work on projects hosted on github.com! Self-hosted GitHub Enterprise Server is not supported. If your repository is private, you must be on the Enterprise Cloud paid plan. For public repositories, GitHub attestations are free.
If these restrictions bother you, other solutions exist for attesting build provenance, e.g. the SLSA Framework’s provenance generator from the OpenSSF Scorecard ecosystem. That project was started in mid-2022 (thus, it’s more mature than GitHub’s solution that launched in May 2024) and has fewer restrictions.
Build provenance attestation in detail
Creating build provenance for one or more files
Before we look at how to create build provenance for containers, let’s start with regular files:
- Somewhere in your GHA workflow, you have a
step
that generates or builds artifacts, which then exist on the file system of the GHA runner. Suppose this step is called the “build” step. This step might also have uploaded the artifacts (e.g. as GitHub release). This is not a problem. It is fine to create attestations after the artifacts are uploaded since the attestation itself is not part of a GitHub release. - Some place after the “build” step, you add a “build provenance” step that uses the actions/attest-build-provenance action. You configure the action to use the file(s) generated by the “build” step. You would typically either set that action’s
subject-path
parameter (which can be a relative path to a specific file, or a folder-path with a glob star, such as
“./dist/*
”), or you provide thesubject-checksums
parameter, which is a relative path to a checksums text file containing the checksums and file names separated by space, see e.g. here. - The
actions/attest-build-provenance
action determines a list of subjects, which is an array of tuples(file-path, SHA256-checksum)
. It then creates a Sigstore attestation for these subjects, which it uploads to GitHub’s attestation API. While doing so, it also creates a Rekor entry (whose ID/URL is shown in the output of the GitHub workflow log). You can inspect this attestation in the web UI of GitHub, see e.g. here. From there, you can also download the attestation as a JSON file.
The attestation looks like this:
From this, we can infer the Rekor log entry (166757272). Decoding the payload
provides information about the covered files (and their hashes) and the build provenance itself:
Verifying build provenance for individual files
Suppose you are the consumer of some CLI that an open-source project offers as precompiled binary in their GitHub releases. Suppose that the project has created build provenance as described above.
To verify the build provenance created by the actions/attest-build-provenance action for a specific file, you first need to install the GitHub CLI (gh
) and be logged into GitHub with it. GitHub’s hosted runners come with the CLI pre-installed, but you can also install it for various OSes with various package managers.
Next, you download the file (e.g. the open-source CLI) you want to verify to disk, e.g. with curl
. You’ll assume that there is a GitHub attestation available for that file. For any project, just open https://github.com/<owner>/<repo>/attestations
to check whether any attestations are available.
Next, run “gh attestation verify <path-to-file> --repo <owner>/<repo>
”. There are various other CLI arguments documented here.
The GitHub CLI then performs these steps:
- It computes the SHA256 checksum of the local file
- It calls
https://api.github.com/orgs/<owner>/attestations/sha256:<hash>
to retrieve existing attestation signatures for that file’s checksum - It verifies that
- the signature is cryptographically valid (that it was really created by GitHub and not by someone else), which includes contacting the Rekor transparency log
- the signature has the correct attestation type (here: build provenance, which is the
https://slsa.dev/provenance/v1
predicate type, which is the implicit default value) - the repository owner and repo name (that you provided as CLI arguments) match the ones from the signature
Alternatively, you could have used the cosign
CLI to do the verification, as also explained in this blog post: you manually download the attestation JSON file from GitHub (e.g. to a file named attestation.json
) and then run the command:
cosign verify-blob-attestation --bundle attestation.json \
--new-bundle-format --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \
--certificate-identity-regexp="^https://github.com/<owner>/<repo>/.*" \
<path-to-file>
Creating build provenance for Docker images
Creating build provenance for Docker images works similarly to creating build provenance for files. Here is an example workflow:
After building and pushing the image, we run the actions/attest-build-provenance
action, this time providing the fully qualified image name (including the server host), the digest (subject-digest
), and that the attestation should also be pushed to the registry.
In contrast to attesting files, Docker images attestations only have one subject: the specific Docker image with its digest (which is a “sha256:…
” string).
As for files, the actions/attest-build-provenance
action creates a Sigstore bundle for this subject, uploads it to GitHub’s attestation API, and also creates the Rekor entry.
As for files, we can inspect this attestation in GitHub’s web UI. From there, you can also download the attestation as JSON.
In addition, because of the “push-to-registry: true
” parameter found in the workflow, the GitHub action also creates and uploads various JSON manifests to the image registry. It creates a new tag named “sha256-<digest>
” with this content:
This manifest essentially emulates the referrers API (see my Notation article for details).
Let’s look more closely at the manifest (851d19398c3…
):
We see an image manifest with a single layer (7c0aad925d…
):
We see a Cosign bundle-spec-formatted manifest which includes the signature, signing certificates, and Rekor log entry information. The most interesting field here is dsseEnvelope → payload
which contains the actual build provenance, formatted using SLSA (https://slsa.dev/provenance/v1
) in a Statement:
Verifying build provenance for Docker images
There are several options to check a Docker image’s build provenance:
- In the terminal (or a CI/CD pipeline), call the GitHub CLI as follows:
gh attestation verify oci://<image-with-tag-or-digest> --repo <owner>/<repo> [--bundle-from-oci]
- Kubernetes operators:
SBOM attestation in detail
Creating SBOM attestations
Above, we used the actions/attest-build-provenance
action to create build provenance attestations. To create SBOM attestations, GitHub offers another official action, actions/attest-sbom. It creates SBOM attestations for files or a Docker image.
For SBOMs, I won’t go into as much detail about how to create them as I did in the above four build provenance-related subsections, because the process is very similar. The only difference is that you must first create the SBOM file, using some tool (e.g. the anchore/sbom-action action). GitHub requires that the SBOM is JSON-formatted, in either the SPDX or CycloneDX format, and that its size does not exceed 16 MB.
The actions/attest-sbom action creates an SBOM attestation in very similar ways as the actions/attest-build-provenance
action creates build provenance. If you look at the resulting attestation, the file formats and meta-data fields of build provenance vs. SBOM attestations are almost equal. The core difference in the Statement’s JSON content is:
Attestation type (a.k.a. Predicate type) | Content of the attestation’s Predicate | |
SLSA build provenance | https://slsa.dev/provenance/v1 | Information about the workflow, e.g. the repository URL or Git commit hash |
SBOM (CycloneDX) | https://cyclonedx.org/bom | The CycloneDX SBOM JSON content |
SBOM (SPDX) | https://spdx.dev/Document/v2.3 | The SPDX SBOM JSON content |
Verifying SBOM attestations
For automatic or manual verification of SBOM attestations, these options exist:
- GitHub CLI: irrespective of whether the SBOM attestation is for file(s) or a Docker image, you can use the “
gh attestation verify
” command, as explained above for build provenance. Make sure to additionally provide the expected SBOM attestation type via the “--predicate-type
” CLI argument (see above table), e.g.
“--predicate-type https://cyclonedx.org/bom
“ - Kubernetes operators:
- Kyverno should support the verification of attestations
- Sigstore Policy controller also supports attestations (see docs)
I don’t think Ratify with OPA Gatekeeper supports the GitHub-generated Cosign bundle-formatted attestations. The documentation only mentions ORAS-attached attestations, whose manifests have a different (incompatible) format.
Attestations of arbitrary type
Apart from build provenance and SBOM attestations, you can also create attestations for any predicate type you desire, using the actions/attest action. Its use is very similar to the actions I presented above. The predicate itself can be any JSON object.
Conclusion
The core benefit I see for GitHub’s attestations is for open-source projects to create build provenance for files (e.g. binaries) that they build using GHA workflows, publishing them via a GitHub release.
I don’t see a strong case for using GitHub attestations for the other combinations, particularly:
- SBOM attestations for files: instead of having dedicated/separate SBOM attestations (using
actions/attest-sbom
), I find it more economical to upload generated SBOMs along with the other binaries you publish via a GitHub release. Then, you configure GitHub’s build provenance attestation (actions/attest-build-provenance
) to cover both the uploaded binaries and the SBOMs as subjects. - Attestations (of any kind) for Docker images: I would instead use the Cosign CLI because its signatures have the widest adoption in the ecosystem (e.g. by Kubernetes operators).
If you consume (download) open-source binaries from GitHub releases, you should check whether the respective maintainers already created attestations. If so, verify the downloaded binaries against the attestation. If the respective maintainers don’t yet provide attestations, offer a PR that adds them. As shown in the above example, a few lines of workflow YAML code are sufficient. In the PR, you could also add documentation explaining how consumers can verify the downloaded binaries.
If attestations exist, verifying them with the GitHub CLI is the easiest approach. However, you must acquire the GitHub CLI in a way that ensures that the CLI has not been tampered with. There are various scenarios:
- You want to download the binaries of third-party CLIs (that you want to verify) directly in your GHA workflows (e.g. using
curl
in arun
step):- If you use GitHub’s hosted runners: the GitHub CLI comes pre-installed with the hosted runners
- If you use self-hosted runners: use sigstore/cosign-installer action to install the cosign CLI, then use the snippet below to acquire a tamper-proof GitHub CLI
- You want to
curl
the third-party CLI binaries in a build-container (e.g. while building aDockerfile
): install the GitHub CLI via a Linux package manager, e.g. in a separate build stage, as shown in the snippet below. You will need a GitHub Personal Access Token (PAT) to use the GitHub CLI. Any PAT will do, it does not need any scopes or permissions.