From 56a57e300892dc344d6f8bbfd233ee9893009efe Mon Sep 17 00:00:00 2001 From: lukasWuttke <54042461+LukasWodka@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:13:20 +0200 Subject: [PATCH 1/3] fix(release): rebuilds must run at the tag ref (Bugbot on promotion #428) Cosign's keyless identity embeds the RUN's ref. workflow_dispatch took an inputs.ref and could build a tag from a branch run, publishing signatures (@refs/heads/...) that the tag-anchored installers reject on every customer machine. inputs.ref removed; dispatch runs now hard-fail unless started at a v* tag ref; rebuild paths = rerun the tag run or dispatch at the tag. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bb79a56..e22de5c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,12 +31,12 @@ on: push: tags: - 'v*.*.*' - workflow_dispatch: - inputs: - ref: - description: 'Tag to build (e.g. v0.1.0). Must already exist on origin.' - required: true - type: string + # Rebuilds: dispatch the workflow AT the tag ref (Actions -> Run workflow -> + # pick the v* tag), or gh run rerun a previous tag run. Cosign embeds the + # RUN's ref in the keyless identity; the installers only trust + # @refs/tags/v.*, so a branch-dispatched "rebuild of a tag" would publish + # signatures every customer install rejects (Bugbot on promotion #428). + workflow_dispatch: {} permissions: contents: write # create / update the GitHub Release @@ -78,7 +78,7 @@ jobs: - name: Checkout uses: actions/checkout@v7 with: - ref: ${{ inputs.ref || github.ref }} + ref: ${{ github.ref }} fetch-depth: 0 - name: Set up Go @@ -95,9 +95,14 @@ jobs: - name: Determine release version id: version run: | - # On a tag push: github.ref_name = "v0.1.0" - # On workflow_dispatch: inputs.ref = "v0.1.0" - REF="${{ inputs.ref || github.ref_name }}" + # Dispatch rebuilds must run AT a v* tag ref -- the keyless identity + # embeds this run's ref, and installers only trust refs/tags/v.*. + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && ! printf '%s' "${{ github.ref }}" | grep -qE '^refs/tags/v'; then + echo "::error::rebuilds must be dispatched from the v* tag itself (Actions -> Run workflow -> select the tag), not a branch -- signatures would embed ${{ github.ref }} and fail every customer verification." + exit 1 + fi + # github.ref_name = "v0.1.0" (tag push, or dispatch AT the tag ref) + REF="${{ github.ref_name }}" # Strip the leading v for use in -X main.version VERSION="${REF#v}" # The VERSION file declares the next release (read by the release @@ -186,7 +191,7 @@ jobs: - name: Checkout uses: actions/checkout@v7 with: - ref: ${{ inputs.ref || github.ref }} + ref: ${{ github.ref }} - name: Download all matrix artifacts uses: actions/download-artifact@v7 @@ -216,7 +221,7 @@ jobs: - name: Determine release tag id: tag run: | - REF="${{ inputs.ref || github.ref_name }}" + REF="${{ github.ref_name }}" # STRICT stability rule: only a plain vX.Y.Z tag is a stable release. # Anything else (v1.2.3-rc.1, and typos like v1.2.3rc1) is marked # prerelease, so it can never become 'latest' -- which is what the From 3736b6aeabd934e2a12c179d8a5a9fdcb3081aea Mon Sep 17 00:00:00 2001 From: lukasWuttke <54042461+LukasWodka@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:38:08 +0200 Subject: [PATCH 2/3] review: pre-flight guard job + refs via env (Asad's nits on #429) Guard moved out of the 8-way matrix into a tiny job that release needs: a branch-misdispatch now fails once in seconds instead of burning eight runners' setup. Refs passed via env, never interpolated -- git permits $/backticks in tag names, so a crafted v* tag would otherwise execute on the runner (R8, same rule as the client installer workflows). Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 31 ++++++++++++++++++++++++------- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e22de5c..a319c07 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,9 +47,27 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: + # Pre-flight: reject a branch-misdispatch ONCE, in seconds, before the 8-way + # matrix spins up (review nit on #429). Ref passed via env, never inline. + guard: + name: Ref guard + runs-on: ubuntu-latest + env: + REF_FULL: ${{ github.ref }} + EVENT_NAME: ${{ github.event_name }} + steps: + - name: Dispatch rebuilds must run at a v* tag ref + run: | + if [ "$EVENT_NAME" = "workflow_dispatch" ] && ! printf '%s' "$REF_FULL" | grep -qE '^refs/tags/v'; then + echo "::error::rebuilds must be dispatched from the v* tag itself (Actions -> Run workflow -> select the tag), not a branch -- signatures would embed $REF_FULL and fail every customer verification." + exit 1 + fi + echo "ref ok: $REF_FULL" + release: timeout-minutes: 20 name: Build + sign + publish + needs: guard runs-on: ubuntu-latest strategy: fail-fast: false @@ -94,15 +112,14 @@ jobs: - name: Determine release version id: version + env: + # Passed via env, never interpolated into the script: git permits $/ + # backticks in tag names and a crafted v* tag would otherwise execute + # on the runner (R8; same rule as the client installer workflows). + REF_NAME: ${{ github.ref_name }} run: | - # Dispatch rebuilds must run AT a v* tag ref -- the keyless identity - # embeds this run's ref, and installers only trust refs/tags/v.*. - if [ "${{ github.event_name }}" = "workflow_dispatch" ] && ! printf '%s' "${{ github.ref }}" | grep -qE '^refs/tags/v'; then - echo "::error::rebuilds must be dispatched from the v* tag itself (Actions -> Run workflow -> select the tag), not a branch -- signatures would embed ${{ github.ref }} and fail every customer verification." - exit 1 - fi # github.ref_name = "v0.1.0" (tag push, or dispatch AT the tag ref) - REF="${{ github.ref_name }}" + REF="$REF_NAME" # Strip the leading v for use in -X main.version VERSION="${REF#v}" # The VERSION file declares the next release (read by the release From b201c110455939076b763458ce0634a6175273e1 Mon Sep 17 00:00:00 2001 From: lukasWuttke <54042461+LukasWodka@users.noreply.github.com> Date: Thu, 30 Jul 2026 10:52:09 +0200 Subject: [PATCH 3/3] fix(release): pass the tag ref via env in Determine-release-tag (R8) The one step this PR's hardening missed: it still did REF="${{ github.ref_name }}", interpolating an attacker-controllable tag name straight into the shell, so a crafted v* tag with backticks or $() would execute on the publish runner before the release is created. Now passed as env REF_NAME and read as $REF_NAME, matching the guard and version steps. --- .github/workflows/release.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a319c07..0a74be6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -237,8 +237,15 @@ jobs: - name: Determine release tag id: tag + env: + # Pass the ref through the environment, never interpolate it into the + # script. A tag name is attacker-controllable, so a crafted v* tag + # carrying backticks or $() would otherwise execute on the publish + # runner before the release is even created (R8). Same treatment the + # guard and version steps above already got. + REF_NAME: ${{ github.ref_name }} run: | - REF="${{ github.ref_name }}" + REF="$REF_NAME" # STRICT stability rule: only a plain vX.Y.Z tag is a stable release. # Anything else (v1.2.3-rc.1, and typos like v1.2.3rc1) is marked # prerelease, so it can never become 'latest' -- which is what the