Skip to content

feat: Add a shared builder workflow for container images - #122

Closed
dervoeti wants to merge 1 commit into
mainfrom
feat/reusable-build-container-image
Closed

feat: Add a shared builder workflow for container images#122
dervoeti wants to merge 1 commit into
mainfrom
feat/reusable-build-container-image

Conversation

@dervoeti

@dervoeti dervoeti commented Aug 6, 2026

Copy link
Copy Markdown
Member

As we found out that https://github.com/slsa-framework/slsa-github-generator will likely be deprecated soon, we decided to use https://github.com/actions/attest instead.

Repositories that build their images in a top-level workflow satisfy SLSA Build Level 2. GitHub's documented mechanism for Build Level 3 is that the build runs in a reusable workflow, separate from the workflow calling it (see https://docs.github.com/en/actions/concepts/security/artifact-attestations#slsa-levels-for-artifact-attestations).

Currently, the operators each build in their own top-level build.yaml, so would be at L2 today. This PR adds the builder they will call. docker-images keeps its own in-repository reusable workflow, because it would need a second one here, so the signer identity count is the same either way (one for operators, one for product images) and the "one identity for the platform" argument does not apply to it. The fact that it has a reusable workflow satisfies GitHub's criterion for SLSA L3, no need to move it here.

reusable_build_container_image.yaml performs the whole chain itself: checks out the caller's source, builds the per-architecture images, pushes and attests each one, assembles the image index from exactly those digests, then signs and attests the index.
The reason for this design is: The digest must never reach the attesting step as an input from the caller, because anyone able to trigger the calling workflow could point it at an image they had pushed themselves and obtain provenance for it under the trusted generator identity. Attestation therefore happens either in the job that pushed the image, using the digest docker push returned, or in a job of this same workflow that receives that digest as a job output. Neither path is reachable by a caller.

So the inputs are source-level build parameters only (Dockerfile path, build context, build arguments, tags, registries), to limit what a potential attacker can provide as input.

Verification would work like this:

gh attestation verify oci://oci.stackable.tech/sdp/kafka-operator:25.7.0 \
  --repo stackabletech/kafka-operator \
  --signer-workflow stackabletech/actions/.github/workflows/reusable_build_container_image.yaml \
  --signer-digest <40-char-builder-commit-sha> \
  --source-ref refs/tags/25.7.0

--signer-workflow on its own is not enough: it takes no ref component and matches any ref, including a branch, so someone could push a branch to this repo with a modified builder and satisfy it.

--signer-digest is what closes that. It pins the exact build definition by its commit hash.

Supporting changes

publish-image exposes the digest it already resolved after pushing, as image-repository-digest and image-digest. The latter is what actions/attest takes as subject-digest, which is how each per-arch image gets its own provenance. docker-images needs this too, for the per-architecture attestation in its own reusable workflow, so this action change unblocks that future PR as well.

publish-image-index-manifest gains image-manifest-digests, so the index is built from the exact digests produced rather than by re-resolving <tag>-<arch> tags.

.github/actionlint.yaml gains ignores for job.workflow_repository and job.workflow_sha. These are documented context properties (https://docs.github.com/en/actions/reference/workflows-and-actions/contexts) and the docs show exactly the actions/checkout usage we rely on, but actionlint does not know them, see rhysd/actionlint#705.

@dervoeti
dervoeti force-pushed the feat/reusable-build-container-image branch from e647534 to 8e95876 Compare August 6, 2026 14:57
@dervoeti dervoeti closed this Aug 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant