3 minutes reading time
I have been waiting for something like this for quite some time, and a few months ago, GitHub rolled out its Artifact Attestations (still in beta). Powered by Sigstore, the idea is simple—these attestations ensure the build environment and the source code. Artifacts are linked to their repositories and the build process with cryptographically signed claims—attestations. Consumers then use these attestations to verify binaries (packages, images, SBOM, etc.) with GitHub's CLI application (v2.49.0
or higher). Presently, there is no other tool besides gh
that could be used to verify attestations. But this should change soon.
Ian Lewis wrote an informative breakdown in which he expands on the internals of the main utility action (actions/attest-build-provenance
).
The attest-build-provenance action is the main tool required for basic integration.
permissions:
id-token: write
contents: read
attestations: write
- name: Attest
uses: actions/attest-build-provenance@v1
with:
subject-path: 'PATH/TO/BINARY'
All set! Swell!
Use sbom-action with attest-sbom to process files like cargo.lock
or requirements.txt
:
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
path: ./
artifact-name: sbom.spdx
- name: Attest SBOM
uses: actions/attest-sbom@v1
with:
subject-path: 'PATH/TO/BINARY'
sbom-path: 'sbom.spdx'
This setup will push images to the container registry (push-to-registry
). Docker's build-push-action could be used to build the image and provide outputs.digest
to the attestation step.
packages
permission:permissions:
id-token: write
contents: read
attestations: write
packages: write
- name: Attest
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
Verify attestations prior usage:
- name: Verify attestation
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh attestation verify PATH/TO/BINARY \
--owner "$GITHUB_REPOSITORY_OWNER"
The attest-build-provenance
action by itself in the regular workflow can meet the requirements for SLSA Build Level 2. To harden the build to L3, the attesting workflow must be isolated from the main workflow:
Prevent secret material used to sign the provenance from being accessible to the user-defined build steps.
This could be achieved with reusable workflows that are executed in separate VMs. I will use my xtxf
repository as an example.
Move all release steps to a new workflow, set on
to a workflow_call
trigger, and add required permissions.
cd.yml
:
name: CD
on:
workflow_call:
inputs:
tag_name:
required: true
type: string
permissions:
id-token: write
contents: read
attestations: read
env:
ZIG_VERSION: 0.13.0
jobs:
release:
name: Create release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: taiki-e/create-gh-release-action@v1
with:
changelog: CHANGELOG.md
token: ${{ secrets.GITHUB_TOKEN }}
build-linux:
name: Build / Linux
runs-on: ubuntu-latest
needs:
permissions:
id-token: write
contents: write
attestations: write
strategy:
fail-fast: false
matrix:
target:
- "x86-linux"
- "x86_64-linux"
- "aarch64-linux"
steps:
- name: Checkout
uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Zig
uses: goto-bus-stop/setup-zig@v2
with:
version: ${{ env.ZIG_VERSION }}
- name: Build
run: |
zig build -Dtarget=${{ matrix.target }} --release=safe
mv zig-out/bin/xtxf xtxf
tar -czvf xtxf-${{ matrix.target}}.tar.gz xtxf
- name: Attest
uses: actions/attest-build-provenance@v1
with:
subject-path: 'xtxf-${{ matrix.target}}.tar.gz'
- name: Upload
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload ${{ inputs.tag_name }} \
xtxf-${{ matrix.target }}.tar.gz
- name: Verify attestation
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh attestation verify xtxf-${{ matrix.target}}.tar.gz \
--owner "$GITHUB_REPOSITORY_OWNER"
Call CD
from a test workflow if all release requirements are met. In this case, version tags, successful test
jobs, and repository owner are used to trigger the release workflow.
ci.yml
:
name: CI
on:
permissions:
id-token: write
contents: read
attestations: read
env:
ZIG_VERSION: 0.13.0
jobs:
test:
name: Test
strategy:
matrix:
os:
mode:
runs-on: ${{ matrix.os }}-latest
steps:
- uses: actions/checkout@v4
- uses: goto-bus-stop/setup-zig@v2
with:
version: ${{ env.ZIG_VERSION }}
- name: Build
run: zig build --release=${{ matrix.mode }} \
--verbose --summary all
- name: Test
run: zig build test --release=${{ matrix.mode }} \
--summary all
release:
name: Release
needs:
permissions:
contents: write
id-token: write
attestations: write
if: |
github.repository_owner == 'charlesrocket' &&
startsWith(github.ref, 'refs/tags/')
uses: ./.github/workflows/cd.yml
with:
tag_name: ${{ github.ref_name }}
Now the attestation steps are isolated in a reusable workflow, satisfying salsa L3 requirements. This workflow will be triggered only by tags in the original repository.
A live version of the example is available here.
Use the repository's Actions management section or a direct link to access the attestations (API).
With forge-proof paper trails, artifact attestations bring new levels of transparency and security to the modern trust model. On the other hand, all attestation workflows have to be executed exclusively by GitHub-owned runners; this would be a dealbreaker for many operations.