Skip to content

Harden workflows: gate self-hosted runner, scope tokens, pin actions - #40

Merged
vinniefalco merged 4 commits into
masterfrom
ci/workflow-hardening
Sep 14, 2026
Merged

vinniefalco merged 4 commits into
masterfrom
ci/workflow-hardening

Conversation

@vinniefalco

Copy link
Copy Markdown
Member

Summary

Security hardening for the Actions setup, from an audit of the workflow files against the standard PR-trigger threat model (fork PRs, secret exposure, self-hosted runners, supply chain).

1. Self-hosted runner reachable from pull requests

stt-miri.yml triggers on pull_request, and its native-whisper job runs on [self-hosted, windows, cuda]. On a public repo, any PR approved to run would compile and execute arbitrary test code on a persistent machine: persistence, runner-credential theft, GPU abuse, and network lateral movement are all in play, and no repository secret needs to be involved for that to hurt. The job now runs only on trusted code:

if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository

Fork PRs keep the two Miri jobs, which run on GitHub-hosted runners. The other self-hosted workflows (whisper-lib.yml, llama-cuda-blackwell.yml) trigger only on push-to-master and dispatch, so they needed no gate.

2. Release workflow token scoped to read by default

promptforge-gateway-v-release.yml triggers on pull_request with contents: write at the top level. Fork PRs are auto-downgraded to read-only, so this was not externally exploitable, but same-repo PRs ran dist plan with a write token. The default is now contents: read; plan, host, and announce grant contents: write themselves (plan keeps write because dist host --steps=create may create the release on tag pushes). Both edits carry HAND-EDIT comments so they get re-applied after cargo dist generate.

3. Script-injection hardening

The plan step interpolated github.ref_name directly into its run: command. Tag names can contain shell metacharacters, so the dist subcommand now travels through DIST_PLAN_COMMAND. The github.ref_name uses in release-workshop.yml were audited and are safe as-is: they flow through env: or action inputs, never through shell interpolation.

4. Third-party actions pinned to commit SHAs

Every non-GitHub action is now pinned to a full commit SHA, with the original ref in a trailing comment:

Action Ref Pinned SHA
dtolnay/rust-toolchain stable (branch) 6bed0761d98439e5a578e2877258200ad565ba87
dtolnay/rust-toolchain nightly (branch) be39649afda95dbf70f87cce95f68b8d5797b296
Swatinem/rust-cache v2 (signed tag, 2.9.2) 6323deb102c322ba6fcbdcafc7e3dddab59af2b6
taiki-e/install-action v2 3f74d7c16a4242f1c95561e98edc25d36adb4375
taiki-e/install-action nextest 4dc1969decfa71b34f25aa7f3dd4656654d9ad1e
tauri-apps/tauri-action v0 (unsigned tag) 84b9d35b5fc46c1e45415bdb6144030364f7ebc5
softprops/action-gh-release v2 3bb12739c298aeb8a4eeaf626c5b8d85266b0e65

The tauri-action pin matters most: it consumes TAURI_SIGNING_PRIVATE_KEY in release-workshop.yml. GitHub-owned actions/* references stay on major-version tags. The dtolnay/rust-toolchain pins track moving branches and need periodic re-pinning; consider Dependabot for github-actions to automate that.

What this PR does not change

  • No behavior change for trusted events: pushes, dispatches, and same-repo PRs run exactly the same jobs as before.
  • Fork PRs lose only the native-whisper self-hosted job; all GitHub-hosted PR jobs are untouched.

Follow-ups outside this PR (repo/org settings, need admin)

  • Settings > Actions > General: set Require approval for all outside collaborators.
  • Settings > Actions > Runners: confirm the windows/cuda self-hosted runner is scoped to this repository only, not the whole cppalliance org.
  • Confirm branch protection on master requires the ci-green check.

Test plan

  • CI on this PR exercises the pinned actions (ci.yml runs the full matrix; stt-miri.yml runs Miri jobs; native-whisper runs because this PR is same-repo).
  • A fork PR after merge shows native-whisper skipped and everything else green.
  • Next gateway-v* tag push completes plan/host/announce with the job-scoped write token.

The `native-whisper` job in `stt-miri.yml` runs on a self-hosted Windows CUDA runner, and the workflow triggers on `pull_request`, so an approved fork PR would execute arbitrary test code on a persistent machine. The job now runs only on pushes, dispatches, and PRs whose head repository is this repository.

- Fork PRs keep the `pure-stt-state` Miri jobs, which run on GitHub-hosted runners.
`promptforge-gateway-v-release.yml` triggers on `pull_request` with `contents: write` at the top level, and its `plan` step interpolates `github.ref_name` directly into a run command. The workflow default is now `contents: read`, the `plan`, `host`, and `announce` jobs grant `contents: write` themselves, and the dist subcommand reaches the shell through `DIST_PLAN_COMMAND` instead of inline interpolation.

- HAND-EDIT comments mark the permission change and the env indirection for re-application after `cargo dist generate`.
- On `pull_request` the `plan` step still runs `dist plan`; only tag pushes receive the `host --steps=create` arguments.
Third-party actions in the PR-triggered and dispatch workflows were referenced through mutable tags and branches, so a retagged release could change what CI executes. Each reference is now pinned to its current commit SHA with the original ref kept in a trailing comment.

- GitHub-owned `actions/*` references stay on major-version tags.
- `Swatinem/rust-cache` resolves to the signed `v2` tag (2.9.2); `tauri-apps/tauri-action` resolves to the unsigned `v0` tag.
- `dtolnay/rust-toolchain` pins track the current heads of the `stable` and `nightly` branches and need periodic re-pinning.
`release-workshop.yml` and `nightly.yml` referenced `dtolnay/rust-toolchain`, `Swatinem/rust-cache`, `tauri-apps/tauri-action`, and `softprops/action-gh-release` through mutable tags. Each reference is now pinned to its current commit SHA with the original ref kept in a trailing comment. The `release-workshop.yml` pins sit on the path that consumes `TAURI_SIGNING_PRIVATE_KEY`.
@vinniefalco
vinniefalco merged commit 0eda0f1 into master Sep 14, 2026
18 checks passed
@vinniefalco
vinniefalco deleted the ci/workflow-hardening branch September 14, 2026 20:25
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