From 4e8c7ef5675c27f5d16934aebd2f07aba1572fbc Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 08:42:49 +0000 Subject: [PATCH 1/4] feat: verify the sind binary before installing it - Download the binary into a temporary directory and install it into `~/.local/bin` only once it passes verification, so an unverified binary is never run. - For sind v0.10.0 and later, compare the binary's sha256 with the release's `checksums.txt` and verify its build provenance attestation with `gh attestation verify`: signer workflow `release.yml`, source ref of the release tag, GitHub-hosted runners only. - Add the inputs `verify` (default `true`) to skip the attestation check on runners without `gh`, and `token` (default `github.token`) for `gh`. The checksum is checked either way. - sind v0.8.0 and v0.9.0 publish neither, so they install unverified with a notice. - On `latest`, e2e runs `install.sh` again with one byte of the binary changed and expects the checksum and the attestation check to reject it. - README lists `gh` as a requirement and documents the new inputs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Vjm6Tjihdc6veGRDr47bTw --- .github/workflows/ci.yml | 61 ++++++++++++++++++++++++++++++++ CLAUDE.md | 9 +++-- README.md | 21 +++++++++++ action.yml | 13 +++++++ scripts/install.sh | 76 ++++++++++++++++++++++++++++++++++++---- 5 files changed, 170 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f25c20e..39e786f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,67 @@ jobs: [[ "$CLUSTERS" == "file,inline" ]] [[ "$VERSION" == v* ]] + # Runs install.sh again with a curl that flips one byte of the + # downloaded binary: the checksum check must reject it, even with + # verify: false, and with checksums.txt rewritten to match, the + # attestation check must. sind v0.8.0 has neither to check. + - name: Check that a modified binary is rejected + if: matrix.version == 'latest' + env: + GH_TOKEN: ${{ github.token }} + SIND_VERSION: ${{ steps.sind.outputs.version }} + run: | + gh --version + stub=$(mktemp -d) + REAL_CURL=$(command -v curl) + export REAL_CURL + cat > "$stub/curl" <<'EOF' + #!/usr/bin/env bash + set -euo pipefail + "$REAL_CURL" "$@" + out= + while (($#)); do + [[ "$1" == --output ]] && out=$2 + shift + done + case "$out" in + */sind-linux-*) + byte=$(od -An -tu1 -j4096 -N1 "$out") + printf "\\$(printf '%03o' $((byte ^ 1)))" | + dd of="$out" bs=1 seek=4096 count=1 conv=notrunc status=none + ;; + */checksums.txt) + if [[ "${MATCH_CHECKSUM:-}" == true ]]; then + (cd "${out%/*}" && sha256sum sind-linux-*) > "$out" + fi + ;; + esac + EOF + chmod +x "$stub/curl" + + installed=$(sha256sum ~/.local/bin/sind) + expect_rejected() { # expected error, env for install.sh + local want=$1 + shift + if env PATH="$stub:$PATH" GITHUB_OUTPUT=/dev/null GITHUB_PATH=/dev/null "$@" \ + bash scripts/install.sh > "$stub/log" 2>&1; then + cat "$stub/log" + echo "::error::install.sh installed a modified binary" + exit 1 + fi + # Show the expected errors without turning them into annotations. + echo "::stop-commands::expected-failure" + cat "$stub/log" + echo "::expected-failure::" + if ! grep -q "$want" "$stub/log"; then + echo "::error::install.sh failed, but not with: ${want}" + exit 1 + fi + } + expect_rejected "Checksum mismatch" SIND_VERIFY=false + expect_rejected "Failed to verify the build provenance attestation" MATCH_CHECKSUM=true + [[ "$(sha256sum ~/.local/bin/sind)" == "$installed" ]] + - name: Run Slurm jobs run: | sind exec file -- sinfo diff --git a/CLAUDE.md b/CLAUDE.md index 8a91bdf..8c4fb84 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -11,8 +11,10 @@ jobs. Personal, uncommitted instructions belong in `CLAUDE.local.md` (gitignored input, and create clusters (`scripts/create.sh`). Outputs: `clusters`, `version`. - `scripts/install.sh`: resolves `latest` from the redirect of the latest release page (not the rate-limited API), enforces `MIN_VERSION`, downloads `sind-linux-amd64` (or - `sind-linux-arm64` on ARM64 runners, sind releases after v0.9.0) into `~/.local/bin`, - and checks `sind version --json`. + `sind-linux-arm64` on ARM64 runners, sind releases after v0.9.0) into a temp dir, + verifies it (sind v0.10.0 and later: `checksums.txt`, and with the `verify` input + `gh attestation verify` using `GH_TOKEN` from the `token` input), and only then + installs it into `~/.local/bin` and checks `sind version --json`. - `scripts/create.sh`: each `clusters` entry is a config file path (scalar) or an inline config (map). It creates each cluster, prints its status, and writes the `clusters` output. @@ -20,7 +22,8 @@ jobs. Personal, uncommitted instructions belong in `CLAUDE.local.md` (gitignored is a warning, so cleanup never fails the job. - `.github/workflows/ci.yml`: `lint` (shellcheck, actionlint) and `e2e`, which runs the action from the checkout against the oldest supported sind release and `latest`, and - against `latest` on an ARM64 runner. + against `latest` on an ARM64 runner. On `latest` it also checks that `install.sh` + rejects a binary with one byte changed. ## Checks diff --git a/README.md b/README.md index f123905..7c18d76 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,8 @@ jobs: - Docker and cgroup v2, which `sind doctor` checks before any cluster is created. - `bash`, `curl`, `jq` and [mikefarah `yq`](https://github.com/mikefarah/yq) v4. +- The [GitHub CLI](https://cli.github.com/) `gh`, which verifies the sind + binary's build provenance attestation (see [Binary verification](#binary-verification)). GitHub's Ubuntu runners meet all of these. @@ -43,10 +45,29 @@ GitHub's Ubuntu runners meet all of these. | Input | Description | Default | |-------|-------------|---------| | `version` | sind version to install (e.g. `v0.9.0`; minimum `v0.8.0`) | `latest` | +| `verify` | Verify the sind binary's build provenance attestation with `gh` (see below) | `true` | +| `token` | GitHub token for `gh` to read the attestations of the public sind repository | `github.token` | | `clusters` | YAML list of cluster definitions (see below) | — | | `pull` | Pull container images before creating | `true` | | `realm` | sind realm for resource isolation (a DNS label, see below) | — | +### Binary verification + +The action downloads the sind binary into a temporary directory and installs +it only once it passes these checks, for sind v0.10.0 and later: + +- Its sha256 checksum must match the release's `checksums.txt`, which catches a + truncated or corrupted download. +- With `verify: true`, the default, its build provenance attestation must show + that sind's release workflow (`.github/workflows/release.yml`) built it from + the release tag on a GitHub-hosted runner. The action checks this with + `gh attestation verify` and `token`; the default `github.token` needs no extra + permissions to read the public sind repository's attestations. + +sind v0.9.0 and older publish neither, so the action installs them unverified +and says so in a notice. On runners without `gh`, such as some self-hosted ones, +set `verify: false` to keep only the checksum check. + ### Cluster definitions Each entry in `clusters` is either a **filepath** to a sind cluster config or diff --git a/action.yml b/action.yml index 901c7d5..d13cbfe 100644 --- a/action.yml +++ b/action.yml @@ -9,6 +9,17 @@ inputs: description: 'sind version to install (e.g., v0.9.0; minimum v0.8.0), or "latest"' required: false default: 'latest' + verify: + description: | + Verify the build provenance attestation of the sind binary (sind v0.10.0 + and later) with the GitHub CLI (gh). Set to "false" on runners without + gh; the binary's checksum is checked either way. + required: false + default: 'true' + token: + description: 'GitHub token for gh to read the attestations of the public sind repository' + required: false + default: ${{ github.token }} clusters: description: | YAML list of cluster definitions. Each entry is either a path to a sind @@ -49,6 +60,8 @@ runs: run: bash "$GITHUB_ACTION_PATH/scripts/install.sh" env: SIND_VERSION: ${{ inputs.version }} + SIND_VERIFY: ${{ inputs.verify }} + GH_TOKEN: ${{ inputs.token }} - name: Set verbosity shell: bash diff --git a/scripts/install.sh b/scripts/install.sh index 5f19f5f..52a348a 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -11,10 +11,17 @@ case "$(uname -s)/$(uname -m)" in ;; esac -REPO_URL="https://github.com/GSI-HPC/sind" +REPO="GSI-HPC/sind" +REPO_URL="https://github.com/${REPO}" VERSION="${SIND_VERSION:-latest}" +VERIFY="${SIND_VERIFY:-true}" CURL_OPTS=(--fail --silent --show-error --location --retry 3 --retry-connrefused) +if [[ "$VERIFY" != "true" && "$VERIFY" != "false" ]]; then + echo "::error::verify must be true or false, got: ${VERIFY}" + exit 1 +fi + # Resolve latest from the redirect of the latest release page. The GitHub API # allows only 60 unauthenticated requests per hour per IP address. if [[ "$VERSION" == "latest" ]]; then @@ -47,18 +54,73 @@ if [[ "$ARCH" == "arm64" && "$(printf '%s\n' "$LAST_AMD64_ONLY" "$VERSION" | sor exit 1 fi +# v0.10.0 and later publish checksums.txt and a build provenance attestation +# for each binary; older releases have neither. +FIRST_ATTESTED="v0.10.0" +attested=false +if [[ "$(printf '%s\n' "$FIRST_ATTESTED" "$VERSION" | sort -V | head -n1)" == "$FIRST_ATTESTED" ]]; then + attested=true +fi + +if [[ "$attested" == "true" && "$VERIFY" == "true" ]] && ! command -v gh >/dev/null; then + echo "::error::Verifying the sind binary's attestation needs the GitHub CLI (gh); install it, or set the input verify: false to skip this check" + exit 1 +fi + echo "Installing sind ${VERSION} (linux/${ARCH})..." -DOWNLOAD_URL="${REPO_URL}/releases/download/${VERSION}/sind-linux-${ARCH}" -INSTALL_DIR="${HOME}/.local/bin" -mkdir -p "$INSTALL_DIR" +ASSET="sind-linux-${ARCH}" +DOWNLOAD_URL="${REPO_URL}/releases/download/${VERSION}" + +# Download and verify in a temporary directory; only a verified binary is +# installed or run. +tmp_dir=$(mktemp -d) +trap 'rm -rf "$tmp_dir"' EXIT +binary="${tmp_dir}/${ASSET}" -if ! curl "${CURL_OPTS[@]}" --output "${INSTALL_DIR}/sind" "$DOWNLOAD_URL"; then - echo "::error::Failed to download sind ${VERSION} from ${DOWNLOAD_URL}" +if ! curl "${CURL_OPTS[@]}" --output "$binary" "${DOWNLOAD_URL}/${ASSET}"; then + echo "::error::Failed to download sind ${VERSION} from ${DOWNLOAD_URL}/${ASSET}" exit 1 fi -chmod +x "${INSTALL_DIR}/sind" +if [[ "$attested" == "true" ]]; then + # The checksum catches a truncated or corrupted download. + if ! curl "${CURL_OPTS[@]}" --output "${tmp_dir}/checksums.txt" "${DOWNLOAD_URL}/checksums.txt"; then + echo "::error::Failed to download the checksums of sind ${VERSION} from ${DOWNLOAD_URL}/checksums.txt" + exit 1 + fi + expected=$(awk -v name="$ASSET" '$2 == name { print $1 }' "${tmp_dir}/checksums.txt") + if [[ ! "$expected" =~ ^[0-9a-f]{64}$ ]]; then + echo "::error::checksums.txt of sind ${VERSION} has no single sha256 checksum for ${ASSET}" + exit 1 + fi + actual=$(sha256sum "$binary" | cut -d ' ' -f 1) + if [[ "$actual" != "$expected" ]]; then + echo "::error::Checksum mismatch for ${ASSET} of sind ${VERSION}: expected ${expected}, got ${actual}; sind was not installed" + exit 1 + fi + echo "Checksum of ${ASSET} matches checksums.txt" + + # The attestation proves that sind's release workflow built the binary + # from the release tag, on a GitHub-hosted runner. + if [[ "$VERIFY" == "true" ]]; then + echo "Verifying the build provenance attestation of ${ASSET}..." + if ! gh attestation verify "$binary" --repo "$REPO" \ + --signer-workflow "${REPO}/.github/workflows/release.yml" \ + --source-ref "refs/tags/${VERSION}" --deny-self-hosted-runners; then + echo "::error::Failed to verify the build provenance attestation of ${ASSET} of sind ${VERSION}; sind was not installed" + exit 1 + fi + else + echo "::notice::Skipping the attestation check of sind ${VERSION} (verify: false)" + fi +else + echo "::notice::sind ${VERSION} has no checksums or attestations (sind ${FIRST_ATTESTED} and later have them); installing it unverified" +fi + +INSTALL_DIR="${HOME}/.local/bin" +mkdir -p "$INSTALL_DIR" +install -m 755 "$binary" "${INSTALL_DIR}/sind" # Make sind available in subsequent steps echo "${INSTALL_DIR}" >> "$GITHUB_PATH" From 9e994b1d03e36ef1c05f2150159eb9d0c244887d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 08:43:29 +0000 Subject: [PATCH 2/4] feat: add a wait input for sind create cluster --wait - New input `wait` (default empty, meaning sind's default): how long `sind create cluster` waits for the nodes and Slurm, e.g. `10m`, or `0` for no limit. sind validates the value. - `create.sh` passes `--wait` on sind releases after v0.10.0, decided from `sind version --json`, and ignores the input with a warning on older ones. - README documents the input, including that the step fails after sind has removed the partly created cluster when the limit expires. - e2e sets `wait: 10m`, covering the warning on older sind and the flag on newer releases. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Vjm6Tjihdc6veGRDr47bTw --- .github/workflows/ci.yml | 2 ++ CLAUDE.md | 5 +++-- README.md | 21 +++++++++++++++++++++ action.yml | 8 ++++++++ scripts/create.sh | 12 +++++++++++- 5 files changed, 45 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 39e786f..ab8c9b9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,8 @@ jobs: with: version: ${{ matrix.version }} realm: e2e + # sind v0.10.0 and older ignore wait with a warning. + wait: 10m clusters: | - .github/e2e/cluster.yml - kind: Cluster diff --git a/CLAUDE.md b/CLAUDE.md index 8c4fb84..cd86bad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,8 +16,9 @@ jobs. Personal, uncommitted instructions belong in `CLAUDE.local.md` (gitignored `gh attestation verify` using `GH_TOKEN` from the `token` input), and only then installs it into `~/.local/bin` and checks `sind version --json`. - `scripts/create.sh`: each `clusters` entry is a config file path (scalar) or an inline - config (map). It creates each cluster, prints its status, and writes the `clusters` - output. + config (map). It creates each cluster (with `--wait` from the `wait` input on sind + releases after v0.10.0, a warning on older ones), prints its status, and writes the + `clusters` output. - `cleanup/action.yml`: `sind delete cluster --all` (in `SIND_REALM`, if set); a failure is a warning, so cleanup never fails the job. - `.github/workflows/ci.yml`: `lint` (shellcheck, actionlint) and `e2e`, which runs the diff --git a/README.md b/README.md index 7c18d76..140bf44 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ GitHub's Ubuntu runners meet all of these. | `token` | GitHub token for `gh` to read the attestations of the public sind repository | `github.token` | | `clusters` | YAML list of cluster definitions (see below) | — | | `pull` | Pull container images before creating | `true` | +| `wait` | How long `sind create cluster` waits for the nodes and Slurm, e.g. `10m`, or `0` for no limit (see below) | sind's default | | `realm` | sind realm for resource isolation (a DNS label, see below) | — | ### Binary verification @@ -85,6 +86,26 @@ clusters: | - worker: 3 ``` +### Wait limit + +`wait` limits how long `sind create cluster` waits for each cluster to become +ready: for the node checks and the Slurm daemons and, for a cluster with +accounting, for its registration with slurmdbd. The limit counts from when the +node containers have started, so image pulls don't count. When it expires, sind +removes the partly created cluster again and the step fails. Leave `wait` empty +for sind's default, or set `0` for no limit. + +```yaml +- uses: GSI-HPC/sind-action@v2 + with: + wait: 10m + clusters: | + - test/cluster.yml +``` + +`sind create cluster --wait` needs a sind release after v0.10.0. With older +ones, the action ignores `wait` with a warning. + ## Outputs | Output | Description | diff --git a/action.yml b/action.yml index d13cbfe..bc38d46 100644 --- a/action.yml +++ b/action.yml @@ -38,6 +38,13 @@ inputs: description: 'Pull container images before creating clusters' required: false default: 'true' + wait: + description: | + How long `sind create cluster` waits for the nodes and Slurm, e.g. "10m", + or "0" for no limit; image pulls don't count. Empty uses sind's default. + Needs a sind release after v0.10.0; older ones ignore it with a warning. + required: false + default: '' realm: description: 'sind realm for resource isolation (useful for parallel jobs); a DNS label: lowercase letters, digits and "-"' required: false @@ -96,3 +103,4 @@ runs: env: SIND_CLUSTERS: ${{ inputs.clusters }} SIND_PULL: ${{ inputs.pull }} + SIND_WAIT: ${{ inputs.wait }} diff --git a/scripts/create.sh b/scripts/create.sh index 7f58194..e292ec6 100755 --- a/scripts/create.sh +++ b/scripts/create.sh @@ -22,6 +22,16 @@ else status_cmd=(status) fi +# `sind create cluster --wait` came after sind v0.10.0; sind validates the value. +wait_flags=() +if [[ -n "${SIND_WAIT:-}" ]]; then + if [[ "$(printf '%s\n' 0.10.0 "$sind_version" | sort -V | tail -n1)" != "0.10.0" ]]; then + wait_flags=(--wait "$SIND_WAIT") + else + echo "::warning::The wait input needs a sind release after v0.10.0 and is ignored with sind ${sind_version}" + fi +fi + # An empty input creates no clusters; anything else must be a list. input_kind=$(yq 'kind' "$input_file") if [[ "$input_kind" != "seq" && "$(yq 'tag' "$input_file")" != "!!null" ]]; then @@ -54,7 +64,7 @@ for ((i = 0; i < count; i++)); do fi # Build command flags - flags=(--config "$config") + flags=(--config "$config" "${wait_flags[@]}") [[ "${SIND_PULL:-false}" == "true" ]] && flags+=(--pull) echo "::group::Creating cluster from $label" From 38c4e9d68d4773460096ac386be63978f76fb899 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 08:44:27 +0000 Subject: [PATCH 3/4] docs: describe the release-tied default node image and Slurm tags - sind releases after v0.10.0 default to the node image published for that release (`sind-node:vX.Y.Z`), so pinning `version` also pins Slurm, and `version: latest` follows the newest release and its image; v0.10.0 and older default to `latest`. - Name the moving `.` and fixed `vX.Y.Z-.` tags, note that `pull: true` fetches the current image behind a moving tag, and show the reproducible form next to the matrix example. - Docker must be rootful (not rootless or userns-remap) on unified cgroup v2, which `sind doctor` checks. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Vjm6Tjihdc6veGRDr47bTw --- README.md | 46 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 140bf44..4cb1d33 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ jobs: The action installs the sind binary for the runner's architecture. ARM64 runners need sind v0.10.0 or later, the first release with linux/arm64 binaries and node images. -- Docker and cgroup v2, which `sind doctor` checks before any cluster is - created. +- A rootful Docker daemon (not rootless or userns-remap) on unified cgroup v2, + which `sind doctor` checks before any cluster is created. - `bash`, `curl`, `jq` and [mikefarah `yq`](https://github.com/mikefarah/yq) v4. - The [GitHub CLI](https://cli.github.com/) `gh`, which verifies the sind binary's build provenance attestation (see [Binary verification](#binary-verification)). @@ -156,10 +156,26 @@ jobs: ## Slurm Versions -Clusters use sind's default node image, `ghcr.io/gsi-hpc/sind-node:latest`, which -carries the newest supported Slurm release line. sind also publishes an image per -supported release line, tagged `.` (e.g. `25.11`). To pin a release line, -or test against several, set the image in the cluster config: +Clusters whose config sets no `defaults.image` use sind's default node image: + +- sind releases after v0.10.0 default to the image published for that release, + `ghcr.io/gsi-hpc/sind-node:vX.Y.Z`, which carries the newest Slurm release + line at the time of that release. Pinning `version` therefore also pins the + Slurm version, and `version: latest` follows the newest sind release and its + image. +- sind v0.10.0 and older default to `ghcr.io/gsi-hpc/sind-node:latest`. + +sind also publishes an image per supported Slurm release line: + +- `.` (e.g. `25.11`) moves: built from sind's `main`, it follows the + newest Slurm release of that line. +- `vX.Y.Z-.` (e.g. `vX.Y.Z-25.11`) is fixed per sind release (after + v0.10.0): its Slurm version stays the same. + +With `pull: true`, the default, every run fetches the current image behind a +moving tag such as `latest` or `.`. + +To test against several release lines, set the image in the cluster config: ```yaml jobs: @@ -188,6 +204,24 @@ jobs: if: always() ``` +The `.` tags move, so a later run may test a newer Slurm release of the +same line. For reproducible runs with a sind release after v0.10.0, pin +`version` and use that release's fixed tags: + +```yaml +- uses: GSI-HPC/sind-action@v2 + with: + version: vX.Y.Z + clusters: | + - kind: Cluster + name: dev + defaults: + image: ghcr.io/gsi-hpc/sind-node:vX.Y.Z-${{ matrix.slurm }} + nodes: + - controller + - worker: 2 +``` + See [Official images](https://gsi-hpc.github.io/sind/container-images/building-images/#official-images) for the available tags. From 6cd8c48ae9c220e2920d02328d3f5da1b3f57f91 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 4 Oct 2026 11:31:20 +0000 Subject: [PATCH 4/4] fix: pin the attestation signer exactly and require gh 2.93.0 - Verify the signing certificate's identity with `--cert-identity` (`release.yml@refs/tags/`) instead of `--signer-workflow`. gh before 2.102.0, which GitHub's runners ship today, matches `--signer-workflow` as a prefix and `--source-ref` case-insensitively (GHSA-wjmr-j3rp-mh2g, GHSA-4mq3-hpgx-9cx8); the exact identity pins the workflow and the tag. - Require gh 2.93.0 or later for the attestation check, which fixes a token leak to TUF mirrors (GHSA-8xvp-7hj6-mcj9), and name `verify: false` in the error. - Set `GH_HOST: github.com` for the install step, so a runner configured for GitHub Enterprise Server still reads the attestations from github.com. - README: the minimum gh version, the hosts gh contacts, and the github.com token needed on GitHub Enterprise Server. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Vjm6Tjihdc6veGRDr47bTw --- README.md | 10 +++++++--- action.yml | 2 ++ scripts/install.sh | 23 ++++++++++++++++++----- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 4cb1d33..12733b0 100644 --- a/README.md +++ b/README.md @@ -35,8 +35,9 @@ jobs: - A rootful Docker daemon (not rootless or userns-remap) on unified cgroup v2, which `sind doctor` checks before any cluster is created. - `bash`, `curl`, `jq` and [mikefarah `yq`](https://github.com/mikefarah/yq) v4. -- The [GitHub CLI](https://cli.github.com/) `gh`, which verifies the sind - binary's build provenance attestation (see [Binary verification](#binary-verification)). +- The [GitHub CLI](https://cli.github.com/) `gh` 2.93.0 or later, which + verifies the sind binary's build provenance attestation (see + [Binary verification](#binary-verification)). GitHub's Ubuntu runners meet all of these. @@ -63,7 +64,10 @@ it only once it passes these checks, for sind v0.10.0 and later: that sind's release workflow (`.github/workflows/release.yml`) built it from the release tag on a GitHub-hosted runner. The action checks this with `gh attestation verify` and `token`; the default `github.token` needs no extra - permissions to read the public sind repository's attestations. + permissions to read the public sind repository's attestations. gh contacts + `api.github.com` and Sigstore's trust roots (`tuf-repo-cdn.sigstore.dev`, + `tuf-repo.github.com`), so runners with an egress allowlist must allow them. + On GitHub Enterprise Server, pass a github.com token as `token`. sind v0.9.0 and older publish neither, so the action installs them unverified and says so in a notice. On runners without `gh`, such as some self-hosted ones, diff --git a/action.yml b/action.yml index bc38d46..8d12c59 100644 --- a/action.yml +++ b/action.yml @@ -69,6 +69,8 @@ runs: SIND_VERSION: ${{ inputs.version }} SIND_VERIFY: ${{ inputs.verify }} GH_TOKEN: ${{ inputs.token }} + # The attestations live on github.com, even on GitHub Enterprise Server. + GH_HOST: github.com - name: Set verbosity shell: bash diff --git a/scripts/install.sh b/scripts/install.sh index 52a348a..01c954b 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -62,9 +62,18 @@ if [[ "$(printf '%s\n' "$FIRST_ATTESTED" "$VERSION" | sort -V | head -n1)" == "$ attested=true fi -if [[ "$attested" == "true" && "$VERIFY" == "true" ]] && ! command -v gh >/dev/null; then - echo "::error::Verifying the sind binary's attestation needs the GitHub CLI (gh); install it, or set the input verify: false to skip this check" - exit 1 +# gh 2.93.0 fixes a token leak to TUF mirrors (GHSA-8xvp-7hj6-mcj9). +MIN_GH_VERSION="2.93.0" +if [[ "$attested" == "true" && "$VERIFY" == "true" ]]; then + if ! command -v gh >/dev/null; then + echo "::error::Verifying the sind binary's attestation needs the GitHub CLI (gh) ${MIN_GH_VERSION} or later; install it, or set the input verify: false to skip this check" + exit 1 + fi + gh_version=$(gh --version | sed -n 's/^gh version \([0-9][0-9.]*\).*/\1/p') + if [[ -z "$gh_version" || "$(printf '%s\n' "$MIN_GH_VERSION" "$gh_version" | sort -V | head -n1)" != "$MIN_GH_VERSION" ]]; then + echo "::error::Verifying the sind binary's attestation needs the GitHub CLI (gh) ${MIN_GH_VERSION} or later, found ${gh_version:-an unknown version}; update it, or set the input verify: false to skip this check" + exit 1 + fi fi echo "Installing sind ${VERSION} (linux/${ARCH})..." @@ -102,11 +111,15 @@ if [[ "$attested" == "true" ]]; then echo "Checksum of ${ASSET} matches checksums.txt" # The attestation proves that sind's release workflow built the binary - # from the release tag, on a GitHub-hosted runner. + # from the release tag, on a GitHub-hosted runner. --cert-identity matches + # the signing certificate's identity exactly; gh before 2.102.0 matches + # --signer-workflow as a prefix and --source-ref case-insensitively + # (GHSA-wjmr-j3rp-mh2g, GHSA-4mq3-hpgx-9cx8). The identity holds the tag + # because release.yml runs on the tag push, not as a reusable workflow. if [[ "$VERIFY" == "true" ]]; then echo "Verifying the build provenance attestation of ${ASSET}..." if ! gh attestation verify "$binary" --repo "$REPO" \ - --signer-workflow "${REPO}/.github/workflows/release.yml" \ + --cert-identity "${REPO_URL}/.github/workflows/release.yml@refs/tags/${VERSION}" \ --source-ref "refs/tags/${VERSION}" --deny-self-hosted-runners; then echo "::error::Failed to verify the build provenance attestation of ${ASSET} of sind ${VERSION}; sind was not installed" exit 1