Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/attest-provenance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Reusable: the SLSA build provenance of the files a caller's release built,
# signed apart from the build (SLSA Build Level 3).
#
# The job runs none of the caller's code: it takes the caller's artifact as
# data, on a runner of its own, and attests it with the steps below. Its
# Sigstore certificate names this workflow as the signer, which only a job of
# this file can get. A build step of the caller, even one that holds
# id-token: write for something else, gets a certificate naming the caller's
# own workflow, so a check that pins this workflow refuses what it signed:
#
# gh attestation verify <file> --repo <owner>/<repo> \
# --signer-workflow IronFlock/workflows/.github/workflows/attest-provenance.yml \
# --source-ref refs/heads/main
#
# Everything in the provenance except the files' names and digests comes from
# the job's OIDC token; SLSA lets the build supply those two. Call it after the
# last step that changes the files, and before they are published.
#
# Container images have their own provenance (release-image.yml).
#
# Callers grant
# permissions: { contents: read, id-token: write, attestations: write }
name: attest-provenance

on:
workflow_call:
inputs:
artifact:
description: Name of the artifact of this workflow run that holds the files.
type: string
required: true
subject-path:
description: >-
The files to attest, one glob per line, relative to the root of the
artifact. Every line must match at least one file.
type: string
required: true
dry-run:
description: Check that the files are there and attest nothing (release pull requests).
type: boolean
default: false

permissions:
contents: read

defaults:
run:
shell: bash

jobs:
attest:
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: ${{ inputs.artifact }}
- name: every pattern matches a file
env:
PATTERNS: ${{ inputs.subject-path }}
run: |
shopt -s nullglob
fail=0
while IFS= read -r pattern; do
[ -n "$pattern" ] || continue
# shellcheck disable=SC2206 # the pattern is there to be expanded
files=($pattern)
if [ "${#files[@]}" = 0 ]; then
echo "::error::no file matches $pattern"
fail=1
continue
fi
sha256sum -- "${files[@]}"
done <<<"$PATTERNS"
exit "$fail"
- name: attest the build provenance
if: ${{ !inputs.dry-run }}
uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
with:
subject-path: ${{ inputs.subject-path }}
29 changes: 28 additions & 1 deletion .github/workflows/selftest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -659,6 +659,33 @@ jobs:
echo "environment claim: '$environment'"
[ "$environment" = image-provenance ] || { echo "::error::no environment claim image-provenance with deployment: false; release-image.yml could not sign"; exit 1; }

# attest-provenance.yml as a dry run: a real one would write to the public
# transparency log on every pull request.
attest-fixture:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- run: mkdir -p out/one && echo image > out/one/one.img.gz && echo bundle > out/one/one.raucb
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: selftest-attest
path: out
retention-days: 1

attest-dry-run:
needs: [attest-fixture]
uses: ./.github/workflows/attest-provenance.yml
permissions:
contents: read
id-token: write
attestations: write
with:
artifact: selftest-attest
subject-path: |
*/*.img.gz
*/*.raucb
dry-run: true

assert-planted:
needs: [security-planted, security-helm, security-rootfs, lint-planted]
# Also when a scanner job broke: the counts then show what is missing.
Expand Down Expand Up @@ -716,7 +743,7 @@ jobs:
# above succeeded (skipped or cancelled counts as failed).
selftest-ok:
if: ${{ always() }}
needs: [lint-self, lint-templates, lint-planted, scripts, gate-pass, gate-fail, node-gate-npm, node-gate-npm-tools, node-gate-bun, node-gate-fail, python-gate-pass, python-gate-fail, shell-gate-pass, shell-gate-artifact, shell-gate-fail, shell-gate-arm64, tools-arm64, security-clean, security-clean-node, security-planted, config-scan-skipped-chart, security-helm, security-rootfs, rootfs-scan-empty-tree, release-dry-run, release-dry-run-package-json, build-context-artifact, release-dry-run-build-context, release-dry-run-image-tag, assert-image-tag, provenance-sign, provenance-environment-claim, assert-planted]
needs: [lint-self, lint-templates, lint-planted, scripts, gate-pass, gate-fail, node-gate-npm, node-gate-npm-tools, node-gate-bun, node-gate-fail, python-gate-pass, python-gate-fail, shell-gate-pass, shell-gate-artifact, shell-gate-fail, shell-gate-arm64, tools-arm64, security-clean, security-clean-node, security-planted, config-scan-skipped-chart, security-helm, security-rootfs, rootfs-scan-empty-tree, release-dry-run, release-dry-run-package-json, build-context-artifact, release-dry-run-build-context, release-dry-run-image-tag, assert-image-tag, provenance-sign, provenance-environment-claim, attest-fixture, attest-dry-run, assert-planted]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ The inputs of each workflow are documented in its `on.workflow_call` block.
| [release-intent.yml](.github/workflows/release-intent.yml) | `intent` | the version file decides: a rise makes a release PR (title `release <name> <tag>` required), an unchanged version a no-release PR; output `kind` gates the release dry run; listed in the `needs` of `ci-ok` |
| [release-image.yml](.github/workflows/release-image.yml) | `verify`, `build`, `publish` | release of a container image on the merge of a release PR into the protected `main`; native multi-arch build, SBOM attached, SLSA provenance signed with a Cloud KMS key and attached (see [Signed image provenance](#signed-image-provenance)), keyless push; on a release PR a dry run (every platform build, no push); re-runs are safe; read rights only; `build-contexts-artifact` hands data an earlier job prepared (with secrets the build must not see) to the build as named contexts |
| [release-tag.yml](.github/workflows/release-tag.yml) | `tag` | the annotated git tag of a release, after every image job of the caller; a tag always means "in the registry"; written with a token of the release GitHub App from the caller's `release` environment |
| [attest-provenance.yml](.github/workflows/attest-provenance.yml) | `attest` | SLSA Build Level 3 provenance of files a release built (disk images, bundles): signed by a job that runs none of the caller's code, under this workflow's identity; pin it with `gh attestation verify --signer-workflow` |
| [selftest.yml](.github/workflows/selftest.yml) | — | proves the harness against [tests/fixtures](tests/fixtures) |

A gate for chart repositories follows the same pattern and is added when the
Expand Down
Loading