SLSA attestations for Docker matrix builds

28Oct25

TL;DR

Supply-chain Levels for Software Artifacts (SLSA) attestations are a great way to show that you care about security, and they’re fairly trivial to add to delivery pipelines that produce a single binary or container image. But things get tricky with matrix jobs that build lots of things in parallel, as you then need to marshal all the metadata into the attestation stage, and there isn’t a straightforward way to do that. It can however be done by generating JSON artifacts alongside images, then munging those into a single document that feeds the attestation process.

Background

Some of the Continuous Delivery (CD) pipelines that I work on at Atsign have got complex. Multiple binaries, from multiple sources, for multiple architectures.

Since Arm runners became available I’ve refactored a bunch or workflows so that the Arm builds run on Arm runners, as they’re much faster than cross compiling with QEMU[1]. But that then means stitching multi-arch images together – more complexity.

I recently spent some time adding Cosign signatures to our images, and that prodded me to get SLSA in place everywhere too. But that meant taking on some complex workflows.

Digest Marshalling

The issue boils down to funnelling the correct image names and corresponding digests into the slsa-github-generator action. There are some good pointers in the documentation, but not quite a complete example for what I needed to do.

Can AI help?

A bit… Gemini got me pointed in the right direction (as it had likely been trained on the generator documentation, and perhaps also some code implementing it). What it didn’t give me was working code. It was trying to write to the same artifact from a matrix job, which works for the first one to finish, and then causes the rest to fail.

We need the image digests for signing anyway

So I can get my digest for cosign and for SLSA in the same step within my docker_combine job:

- name: Save digest to file and sign combined manifests
  id: save_digest
  run: |
    IMAGE="atsigncompany/${{ matrix.name }}:${{ env.TAG1 }}"
    IMAGE_DIGEST=$(docker buildx imagetools inspect ${IMAGE} \
      --format "{{json .Manifest}}" | jq -r .digest)
    # Create a JSON object for the image and digest
    echo "{\"name\": \"${IMAGE}\", \"digest\": \"${IMAGE_DIGEST}\"}" \
      > ${{ matrix.name }}_digest.json
    IMAGES="${IMAGE}@${IMAGE_DIGEST}"
    IMAGES+=" atsigncompany/${{ matrix.name }}:${{ env.TAG2 }}@${IMAGE_DIGEST}"
    cosign sign --yes ${IMAGES}

Then upload them as (uniquely named) artifacts

- name: Upload image digest file
  uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
  with:
    name: digests-${{ matrix.name }}
    path: ./${{ matrix.name }}_digest.json

Then aggregate the digests into a JSON document

This is slightly fiddly, as if I send the JSON straight to $GITHUB_OUTPUT the first line break will be treated as the end, and the rest of the JSON will be lost, so I need to follow the process for multiline strings.

aggregate_digests:
  runs-on: ubuntu-latest
  needs: [docker_combine]
  outputs:
    slsa_matrix: ${{ steps.create_matrix.outputs.matrix_json }}
  steps:
    - name: Download all-image-digests artifact
      uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
      with:
        pattern: digests-*
        path: ./digests
        merge-multiple: true
    - name: Combine digests into a single JSON array
      id: create_matrix
      run: |
        MATRIX_JSON=$(jq -s '.' ./digests/*_digest.json)
        {
          echo "matrix_json<<EOF"
          echo "${MATRIX_JSON}"
          echo "EOF"
        } >> "$GITHUB_OUTPUT"
        echo "::notice::Generated SLSA Matrix JSON: ${MATRIX_JSON}"

In better news Gemini did come up with the right jq expression :)

And finally pass the JSON into the slsa-github-generator

The crucial bit here is creating a matrix ‘image_data’ from the JSON and then using the ‘name’ and ‘digest’ elements.

slsa_provenance:
  needs: [aggregate_digests]
  permissions:
    actions: read # for detecting the Github Actions environment.
    id-token: write # for creating OIDC tokens for signing.
    packages: write # for uploading attestations.
  strategy:
    matrix:
      image_data: ${{ fromJson(needs.aggregate_digests.outputs.slsa_matrix) }}
  uses: slsa-framework/slsa-github-generator/.github/workflows/[email protected]
  with:
    image: ${{ matrix.image_data.name }}
    digest: ${{ matrix.image_data.digest }}

Have I missed a trick here?

Could this be done directly with step outputs from the matrix into the SLSA generator (and without squirting JSON into artifacts etc.)? If you have the wizardly incantations to do that I’d love to hear about them.

Note

1. Most of the stuff I’m working with is Dart based, and usually the slow bit (especially in QEMU) is ‘dart compile’. Since Dart can now do cross compilation it’s possible that I could refactor things once more, but I haven’t got around to that yet.



No Responses Yet to “SLSA attestations for Docker matrix builds”

  1. Leave a Comment

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.