From 69bccf357e2a1ba370ba72530a20c65076bb4acf Mon Sep 17 00:00:00 2001 From: Raman Tehlan Date: Wed, 15 Jul 2026 19:24:29 +0530 Subject: [PATCH 1/3] add support for signing image and SBOM Signed-off-by: Raman Tehlan --- .github/workflows/build.yml | 69 +++++++++++++++++++++++++++++++++++++ README.md | 56 ++++++++++++++++++++++++++++-- 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 15ca16e..814405e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -98,6 +98,16 @@ on: required: false type: boolean default: false + enable_sign: + description: 'Sign the pushed image(s) by digest with cosign (keyless/Sigstore). Requires the calling job to grant id-token: write.' + required: false + type: boolean + default: false + enable_sbom: + description: 'Generate an SPDX SBOM for the pushed image(s) and attach it as a cosign attestation. Requires the calling job to grant id-token: write.' + required: false + type: boolean + default: false checkout_submodules: description: 'Whether to checkout git submodules: "true", "recursive", or "false"' required: false @@ -130,6 +140,8 @@ jobs: build: name: Build and Push Image to Repository runs-on: ubuntu-latest + outputs: + digest: ${{ steps.build.outputs.digest }} env: JFROG_IMAGE_URL: ${{ inputs.artifactory_repository_url }}/${{ inputs.image_artifact_name }} ECR_IMAGE_URL: ${{ inputs.ecr_repository_url }}/${{ inputs.image_artifact_name }} @@ -306,3 +318,60 @@ jobs: - name: Output image digest run: | echo "Image digest: ${{ steps.build.outputs.digest }}" + + - name: Install cosign + if: ${{ inputs.enable_sign || inputs.enable_sbom }} + uses: sigstore/cosign-installer@v3 + + - name: Install Syft + if: ${{ inputs.enable_sbom }} + uses: anchore/sbom-action/download-syft@v0 + + - name: Sign image(s) with cosign (keyless) + if: ${{ inputs.enable_sign }} + env: + COSIGN_YES: 'true' + DIGEST: ${{ steps.build.outputs.digest }} + ENABLE_JFROG: ${{ inputs.enable_jfrog }} + ENABLE_ECR: ${{ inputs.enable_public_ecr }} + run: | + set -euo pipefail + if [ -z "$DIGEST" ]; then + echo "Error: image digest is empty, cannot sign" >&2 + exit 1 + fi + if [ "$ENABLE_JFROG" == "true" ]; then + echo "Signing ${JFROG_IMAGE_URL}@${DIGEST}" + cosign sign "${JFROG_IMAGE_URL}@${DIGEST}" + fi + if [ "$ENABLE_ECR" == "true" ]; then + echo "Signing ${ECR_IMAGE_URL}@${DIGEST}" + cosign sign "${ECR_IMAGE_URL}@${DIGEST}" + fi + + - name: Generate and attest SBOM (keyless) + if: ${{ inputs.enable_sbom }} + env: + COSIGN_YES: 'true' + DIGEST: ${{ steps.build.outputs.digest }} + ENABLE_JFROG: ${{ inputs.enable_jfrog }} + ENABLE_ECR: ${{ inputs.enable_public_ecr }} + run: | + set -euo pipefail + if [ -z "$DIGEST" ]; then + echo "Error: image digest is empty, cannot generate SBOM" >&2 + exit 1 + fi + attest_sbom() { + ref="$1" + echo "Generating SBOM for ${ref}" + syft scan "registry:${ref}" -o spdx-json=sbom.spdx.json + echo "Attesting SBOM for ${ref}" + cosign attest --predicate sbom.spdx.json --type spdxjson "${ref}" + } + if [ "$ENABLE_JFROG" == "true" ]; then + attest_sbom "${JFROG_IMAGE_URL}@${DIGEST}" + fi + if [ "$ENABLE_ECR" == "true" ]; then + attest_sbom "${ECR_IMAGE_URL}@${DIGEST}" + fi diff --git a/README.md b/README.md index b9bdeb7..9faca44 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,10 @@ jobs: Builds a multi-platform Docker image and pushes it to JFrog Artifactory and/or AWS Public ECR. Optionally frees runner disk space, scans the `amd64` image with Anchore Grype before -pushing, applies extra tags, and emits provenance attestation. Uses a registry-based build -cache. +pushing, applies extra tags, and emits provenance attestation. Can also keyless-sign the +pushed image(s) with [cosign](https://github.com/sigstore/cosign) and attach an SPDX SBOM +(generated by [Syft](https://github.com/anchore/syft)) as a cosign attestation. Uses a +registry-based build cache. **Usage** @@ -84,6 +86,8 @@ jobs: | `free_disk_space_tool_cache_storage` | Free disk space used by the tool cache | boolean | false | `false` | | `free_disk_space_large_packages` | Free disk space used by large packages | boolean | false | `false` | | `enable_provenance` | Enable provenance attestation for supply-chain security | boolean | false | `false` | +| `enable_sign` | Keyless-sign the pushed image(s) by digest with cosign | boolean | false | `false` | +| `enable_sbom` | Generate an SPDX SBOM per image (Syft) and attach it as a cosign attestation | boolean | false | `false` | **Secrets** @@ -93,6 +97,54 @@ jobs: | `artifactory_password` | Password for JFrog Artifactory | Required if `enable_jfrog` | | `ecr_role_arn` | Role ARN to pull/push images to AWS Public ECR | Required if `enable_public_ecr` | +**Outputs** + +| Name | Description | +| -------- | ------------------------------------------------- | +| `digest` | Digest (`sha256:…`) of the pushed image manifest | + +**Signing & SBOM (cosign / Syft)** + +`enable_sign` and `enable_sbom` use **keyless** Sigstore signing: the identity is bound to +the GitHub Actions OIDC token and recorded in the public Rekor transparency log — there is no +long-lived key to manage. Signatures/attestations are stored in the same registry as the +image, so the login credentials already provided are reused. + +Because keyless signing needs an OIDC token, the **calling job must grant `id-token: write`** +(the reusable workflow inherits the caller's permissions and cannot elevate them): + +```yaml +jobs: + build: + permissions: + contents: read + id-token: write # required for keyless cosign signing / SBOM attestation + uses: truefoundry/github-workflows-public/.github/workflows/build.yml@main + with: + artifactory_registry_url: tfy.jfrog.io + artifactory_repository_url: tfy.jfrog.io/tfy-images + image_artifact_name: mlfoundry-server + image_tag: ${{ github.sha }} + enable_sign: true + enable_sbom: true + secrets: + artifactory_username: ${{ secrets.ARTIFACTORY_USERNAME }} + artifactory_password: ${{ secrets.ARTIFACTORY_PASSWORD }} +``` + +Verify a signed image: + +```bash +cosign verify /@ \ + --certificate-identity-regexp 'https://github.com///.github/workflows/.*@refs/tags/.*' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com + +# Inspect the attached SBOM attestation +cosign verify-attestation /@ --type spdxjson \ + --certificate-identity-regexp 'https://github.com///.github/workflows/.*@refs/tags/.*' \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com +``` + --- ### `build-and-push-soci-index.yml` — Build and push SOCI index From cb84f88584b156183a752627817c83589a9acce4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 16 Jul 2026 07:51:51 +0000 Subject: [PATCH 2/3] Expose digest via workflow_call outputs Wire jobs.build.outputs.digest through on.workflow_call.outputs so caller workflows can read needs..outputs.digest as documented. Co-authored-by: Raman Tehlan --- .github/workflows/build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 814405e..9b17b13 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -118,6 +118,10 @@ on: required: false type: number default: 1 + outputs: + digest: + description: 'Digest (sha256:…) of the pushed image manifest' + value: ${{ jobs.build.outputs.digest }} secrets: artifactory_username: description: 'Username for JFrog Artifactory. Required if enable_jfrog is true' From 724aa1687da378f27239a7b8a24b6d2984fbe31e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 16 Jul 2026 08:51:24 +0000 Subject: [PATCH 3/3] Fix Syft invocation to use download-syft cmd output anchore/sbom-action/download-syft exposes the binary via the cmd output rather than relying on PATH. Give the install step an id and invoke that path so enable_sbom does not fail with command not found. Co-authored-by: Raman Tehlan --- .github/workflows/build.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b17b13..44ced81 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -328,6 +328,7 @@ jobs: uses: sigstore/cosign-installer@v3 - name: Install Syft + id: syft if: ${{ inputs.enable_sbom }} uses: anchore/sbom-action/download-syft@v0 @@ -360,16 +361,21 @@ jobs: DIGEST: ${{ steps.build.outputs.digest }} ENABLE_JFROG: ${{ inputs.enable_jfrog }} ENABLE_ECR: ${{ inputs.enable_public_ecr }} + SYFT: ${{ steps.syft.outputs.cmd }} run: | set -euo pipefail if [ -z "$DIGEST" ]; then echo "Error: image digest is empty, cannot generate SBOM" >&2 exit 1 fi + if [ -z "$SYFT" ]; then + echo "Error: Syft command path is empty, cannot generate SBOM" >&2 + exit 1 + fi attest_sbom() { ref="$1" echo "Generating SBOM for ${ref}" - syft scan "registry:${ref}" -o spdx-json=sbom.spdx.json + "$SYFT" scan "registry:${ref}" -o spdx-json=sbom.spdx.json echo "Attesting SBOM for ${ref}" cosign attest --predicate sbom.spdx.json --type spdxjson "${ref}" }