From b43f66208bc75150260ad038cd49b81613bed3ce Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:26:10 +0000 Subject: [PATCH 1/8] attest images pushed by ci --- .github/workflows/ci.yml | 6 ++++ .github/workflows/internal-push.yml | 48 +++++++++++++++++++++++++++++ README.md | 16 ++++++++++ 3 files changed, 70 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09cd91c04..3e22b1eb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,6 +120,12 @@ jobs: if: ((github.event_name == 'push' || github.event_name == 'schedule') && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) name: 5 push needs: [setup, build] + permissions: + attestations: write + contents: read + id-token: write + packages: write + statuses: write uses: ./.github/workflows/internal-push.yml with: sha: ${{ needs.setup.outputs.sha }} diff --git a/.github/workflows/internal-push.yml b/.github/workflows/internal-push.yml index bda89e077..0373a6984 100644 --- a/.github/workflows/internal-push.yml +++ b/.github/workflows/internal-push.yml @@ -59,6 +59,8 @@ jobs: fail-fast: false name: 1 push (${{ matrix.tag }}, ${{ matrix.arch }}) permissions: + attestations: write + id-token: write packages: write statuses: write runs-on: ubuntu-latest @@ -87,6 +89,20 @@ jobs: run: > docker push ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} + - name: Image Digest + id: digest + run: | + digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} | jq -r '.digest')" + if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "could not determine digest of pushed image" >&2 + exit 1 + fi + echo "digest=$digest" | tee -a $GITHUB_OUTPUT + - name: Attest Image + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-name: ${{ secrets.registry }}/${{ inputs.registry_repo }} + subject-digest: ${{ steps.digest.outputs.digest }} - name: Post Status with Image Name uses: actions/github-script@v5 with: @@ -108,6 +124,8 @@ jobs: fail-fast: false name: 2 push manifest (${{ matrix.tag }}) permissions: + attestations: write + id-token: write packages: write statuses: write runs-on: ubuntu-latest @@ -137,6 +155,20 @@ jobs: run: > docker manifest push ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} + - name: Image Digest + id: digest + run: | + digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} | jq -r '.digest')" + if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "could not determine digest of pushed image" >&2 + exit 1 + fi + echo "digest=$digest" | tee -a $GITHUB_OUTPUT + - name: Attest Image + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-name: ${{ secrets.registry }}/${{ inputs.registry_repo }} + subject-digest: ${{ steps.digest.outputs.digest }} - uses: actions/github-script@v5 with: script: | @@ -157,6 +189,8 @@ jobs: fail-fast: false name: 3 push alias (${{ matrix.tag }}) permissions: + attestations: write + id-token: write packages: write statuses: write runs-on: ubuntu-latest @@ -176,6 +210,20 @@ jobs: docker buildx imagetools create -t ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag-alias }} ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} + - name: Image Digest + id: digest + run: | + digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag-alias }} | jq -r '.digest')" + if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then + echo "could not determine digest of pushed image" >&2 + exit 1 + fi + echo "digest=$digest" | tee -a $GITHUB_OUTPUT + - name: Attest Image + uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 + with: + subject-name: ${{ secrets.registry }}/${{ inputs.registry_repo }} + subject-digest: ${{ steps.digest.outputs.digest }} - uses: actions/github-script@v5 with: script: | diff --git a/README.md b/README.md index 19c0bdfb1..45b7011f3 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,22 @@ All tags are published as multiplatform images supporting architectures: - `linux/amd46` - `linux/arm64` +## Verifying Images + +Images are published with [build provenance attestations] signed by GitHub +Actions, recording the workflow and commit that built them. Verify an image with +the [`gh` CLI]: + +``` +gh attestation verify oci://docker.io/stellar/quickstart:latest --repo stellar/quickstart +``` + +Attestations are only available for images built after attestation support was +added to the build, and are not available for older images. + +[build provenance attestations]: https://docs.github.com/en/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds +[`gh` CLI]: https://cli.github.com + ## Usage To use this project successfully, you should first decide a few things: From 171ea29b61507809fabc1a7562cbc73319878a50 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:37:18 +0000 Subject: [PATCH 2/8] note gh auth and why alias is attested --- .github/workflows/internal-push.yml | 4 ++++ README.md | 3 +++ 2 files changed, 7 insertions(+) diff --git a/.github/workflows/internal-push.yml b/.github/workflows/internal-push.yml index 0373a6984..13b6a2684 100644 --- a/.github/workflows/internal-push.yml +++ b/.github/workflows/internal-push.yml @@ -219,6 +219,10 @@ jobs: exit 1 fi echo "digest=$digest" | tee -a $GITHUB_OUTPUT + # Redundant while the alias is a 1:1 copy of a manifest list, because + # imagetools create passes the original bytes and descriptor through and so + # the alias has the same digest as the manifest attested above. Kept so the + # alias tag stays attested if that ever stops holding. - name: Attest Image uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: diff --git a/README.md b/README.md index 45b7011f3..c3be322e2 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,9 @@ the [`gh` CLI]: gh attestation verify oci://docker.io/stellar/quickstart:latest --repo stellar/quickstart ``` +The `gh` CLI must be authenticated, because the attestation is fetched from +GitHub rather than from the registry. + Attestations are only available for images built after attestation support was added to the build, and are not available for older images. From b41433ce2fb8f1166b40297b0b4202df603b36d5 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:43:51 +0000 Subject: [PATCH 3/8] rely on the attest action to check the digest --- .github/workflows/internal-push.yml | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/.github/workflows/internal-push.yml b/.github/workflows/internal-push.yml index 13b6a2684..6b5f2311c 100644 --- a/.github/workflows/internal-push.yml +++ b/.github/workflows/internal-push.yml @@ -92,11 +92,8 @@ jobs: - name: Image Digest id: digest run: | + set -o pipefail digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} | jq -r '.digest')" - if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "could not determine digest of pushed image" >&2 - exit 1 - fi echo "digest=$digest" | tee -a $GITHUB_OUTPUT - name: Attest Image uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 @@ -158,11 +155,8 @@ jobs: - name: Image Digest id: digest run: | + set -o pipefail digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} | jq -r '.digest')" - if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "could not determine digest of pushed image" >&2 - exit 1 - fi echo "digest=$digest" | tee -a $GITHUB_OUTPUT - name: Attest Image uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 @@ -213,11 +207,8 @@ jobs: - name: Image Digest id: digest run: | + set -o pipefail digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag-alias }} | jq -r '.digest')" - if [[ ! "$digest" =~ ^sha256:[0-9a-f]{64}$ ]]; then - echo "could not determine digest of pushed image" >&2 - exit 1 - fi echo "digest=$digest" | tee -a $GITHUB_OUTPUT # Redundant while the alias is a 1:1 copy of a manifest list, because # imagetools create passes the original bytes and descriptor through and so From fb26673a8acd847aa72cf486a7713fe7d79126b1 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:56:38 +0000 Subject: [PATCH 4/8] drop comment on the alias attestation --- .github/workflows/internal-push.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/internal-push.yml b/.github/workflows/internal-push.yml index 6b5f2311c..9d9fdd56d 100644 --- a/.github/workflows/internal-push.yml +++ b/.github/workflows/internal-push.yml @@ -210,10 +210,6 @@ jobs: set -o pipefail digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag-alias }} | jq -r '.digest')" echo "digest=$digest" | tee -a $GITHUB_OUTPUT - # Redundant while the alias is a 1:1 copy of a manifest list, because - # imagetools create passes the original bytes and descriptor through and so - # the alias has the same digest as the manifest attested above. Kept so the - # alias tag stays attested if that ever stops holding. - name: Attest Image uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: From d84423c25cde1383d8c0e493e6a930a228333571 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:00:34 +0000 Subject: [PATCH 5/8] attest only images pushed from main --- .github/workflows/internal-push.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/internal-push.yml b/.github/workflows/internal-push.yml index 9d9fdd56d..a755c8321 100644 --- a/.github/workflows/internal-push.yml +++ b/.github/workflows/internal-push.yml @@ -91,11 +91,13 @@ jobs: ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} - name: Image Digest id: digest + if: github.ref == 'refs/heads/main' run: | set -o pipefail digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} | jq -r '.digest')" echo "digest=$digest" | tee -a $GITHUB_OUTPUT - name: Attest Image + if: github.ref == 'refs/heads/main' uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-name: ${{ secrets.registry }}/${{ inputs.registry_repo }} @@ -154,11 +156,13 @@ jobs: ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} - name: Image Digest id: digest + if: github.ref == 'refs/heads/main' run: | set -o pipefail digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} | jq -r '.digest')" echo "digest=$digest" | tee -a $GITHUB_OUTPUT - name: Attest Image + if: github.ref == 'refs/heads/main' uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-name: ${{ secrets.registry }}/${{ inputs.registry_repo }} @@ -206,11 +210,13 @@ jobs: ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} - name: Image Digest id: digest + if: github.ref == 'refs/heads/main' run: | set -o pipefail digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag-alias }} | jq -r '.digest')" echo "digest=$digest" | tee -a $GITHUB_OUTPUT - name: Attest Image + if: github.ref == 'refs/heads/main' uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-name: ${{ secrets.registry }}/${{ inputs.registry_repo }} From ab3ce936506d7633226b16be17abb066d14f3895 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:43:51 +0000 Subject: [PATCH 6/8] also attest on pr 977 to test it --- .github/workflows/internal-push.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/internal-push.yml b/.github/workflows/internal-push.yml index a755c8321..ace5e7fec 100644 --- a/.github/workflows/internal-push.yml +++ b/.github/workflows/internal-push.yml @@ -91,13 +91,13 @@ jobs: ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} - name: Image Digest id: digest - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' || github.ref == 'refs/pull/977/merge' run: | set -o pipefail digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} | jq -r '.digest')" echo "digest=$digest" | tee -a $GITHUB_OUTPUT - name: Attest Image - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' || github.ref == 'refs/pull/977/merge' uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-name: ${{ secrets.registry }}/${{ inputs.registry_repo }} @@ -156,13 +156,13 @@ jobs: ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} - name: Image Digest id: digest - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' || github.ref == 'refs/pull/977/merge' run: | set -o pipefail digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} | jq -r '.digest')" echo "digest=$digest" | tee -a $GITHUB_OUTPUT - name: Attest Image - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' || github.ref == 'refs/pull/977/merge' uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-name: ${{ secrets.registry }}/${{ inputs.registry_repo }} @@ -210,13 +210,13 @@ jobs: ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} - name: Image Digest id: digest - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' || github.ref == 'refs/pull/977/merge' run: | set -o pipefail digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag-alias }} | jq -r '.digest')" echo "digest=$digest" | tee -a $GITHUB_OUTPUT - name: Attest Image - if: github.ref == 'refs/heads/main' + if: github.ref == 'refs/heads/main' || github.ref == 'refs/pull/977/merge' uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-name: ${{ secrets.registry }}/${{ inputs.registry_repo }} From fa0bb5332681c2c2465b226f7c85df9378addfbb Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:58:41 +0000 Subject: [PATCH 7/8] stop attesting on pr 977 --- .github/workflows/internal-push.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/internal-push.yml b/.github/workflows/internal-push.yml index ace5e7fec..a755c8321 100644 --- a/.github/workflows/internal-push.yml +++ b/.github/workflows/internal-push.yml @@ -91,13 +91,13 @@ jobs: ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} - name: Image Digest id: digest - if: github.ref == 'refs/heads/main' || github.ref == 'refs/pull/977/merge' + if: github.ref == 'refs/heads/main' run: | set -o pipefail digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} | jq -r '.digest')" echo "digest=$digest" | tee -a $GITHUB_OUTPUT - name: Attest Image - if: github.ref == 'refs/heads/main' || github.ref == 'refs/pull/977/merge' + if: github.ref == 'refs/heads/main' uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-name: ${{ secrets.registry }}/${{ inputs.registry_repo }} @@ -156,13 +156,13 @@ jobs: ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} - name: Image Digest id: digest - if: github.ref == 'refs/heads/main' || github.ref == 'refs/pull/977/merge' + if: github.ref == 'refs/heads/main' run: | set -o pipefail digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} | jq -r '.digest')" echo "digest=$digest" | tee -a $GITHUB_OUTPUT - name: Attest Image - if: github.ref == 'refs/heads/main' || github.ref == 'refs/pull/977/merge' + if: github.ref == 'refs/heads/main' uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-name: ${{ secrets.registry }}/${{ inputs.registry_repo }} @@ -210,13 +210,13 @@ jobs: ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} - name: Image Digest id: digest - if: github.ref == 'refs/heads/main' || github.ref == 'refs/pull/977/merge' + if: github.ref == 'refs/heads/main' run: | set -o pipefail digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag-alias }} | jq -r '.digest')" echo "digest=$digest" | tee -a $GITHUB_OUTPUT - name: Attest Image - if: github.ref == 'refs/heads/main' || github.ref == 'refs/pull/977/merge' + if: github.ref == 'refs/heads/main' uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 with: subject-name: ${{ secrets.registry }}/${{ inputs.registry_repo }} From 1333e742d4ac111bdd322aa470c0eaa7b36f02d9 Mon Sep 17 00:00:00 2001 From: Leigh <351529+leighmcculloch@users.noreply.github.com> Date: Wed, 26 Aug 2026 13:08:17 +0000 Subject: [PATCH 8/8] drop the redundant alias attestation --- .github/workflows/internal-push.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/internal-push.yml b/.github/workflows/internal-push.yml index a755c8321..354e56672 100644 --- a/.github/workflows/internal-push.yml +++ b/.github/workflows/internal-push.yml @@ -187,8 +187,6 @@ jobs: fail-fast: false name: 3 push alias (${{ matrix.tag }}) permissions: - attestations: write - id-token: write packages: write statuses: write runs-on: ubuntu-latest @@ -208,19 +206,6 @@ jobs: docker buildx imagetools create -t ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag-alias }} ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag }} - - name: Image Digest - id: digest - if: github.ref == 'refs/heads/main' - run: | - set -o pipefail - digest="$(docker buildx imagetools inspect --format '{{json .Manifest}}' ${{ secrets.registry }}/${{ inputs.registry_repo }}:${{ steps.tag.outputs.tag-alias }} | jq -r '.digest')" - echo "digest=$digest" | tee -a $GITHUB_OUTPUT - - name: Attest Image - if: github.ref == 'refs/heads/main' - uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2 - with: - subject-name: ${{ secrets.registry }}/${{ inputs.registry_repo }} - subject-digest: ${{ steps.digest.outputs.digest }} - uses: actions/github-script@v5 with: script: |