CI: hardens the workflows (SHA pins, scoped secrets and token permissions) - #2492
Conversation
A version tag like @v1 or a branch like @master is a movable pointer: whoever controls the action repository can re-point it, and the next run executes whatever it points at. Several of these actions run in jobs whose outputs ship to users: build-release.yml creates the GitHub release with the downloadable binaries, and build-windows-hosts.yml holds the SignPath signing token, so a re-pointed tag there could ship a tampered or wrongly signed release. This is exactly how the tj-actions/changed-files compromise propagated (CVE-2025-30066). Pinning by full commit SHA makes the reviewed code the code that runs, the same way docker/setup-qemu-action is already pinned in build-linux-vms.yml. Refs that pointed at master (cross-platform-actions, setup-alpine, get-latest-release) are pinned to their latest release tag. Every pin keeps a version comment, and each SHA was resolved from the upstream repository and cross-checked against its release tag.
Every reusable workflow call used 'secrets: inherit', which hands the caller's whole secret set to the called workflow, but the only secret any of them reads is SIGNPATH_API_TOKEN in build-windows-hosts.yml. Passing that one secret explicitly and dropping inherit everywhere else means a compromised step in, say, a BSD build VM has no signing token to steal, and the workflow files now show exactly which secret flows where. The secret is declared optional in the reusable workflow so runs without it (pull requests, forks) behave as before; the signing step is already guarded to upstream push events.
Jobs without a permissions block get the repository's default token scope, and reusable workflow calls pass the caller's grants straight through. Scopes are now derived from what each workflow actually does with the token: - every build workflow declares contents: read; none of them writes through the GITHUB_TOKEN. - build-linux-hosts.yml keeps security-events: write for the CodeQL upload; the other callers had that grant too but never upload scanning results, so they drop it. - build-windows-hosts.yml gets actions: read, which the SignPath action documents needing to read job details and download the unsigned artifact. - build-release.yml declares contents: write, matching the grant its caller already makes for creating the release. Runs that execute pull request code now hold a token that can do nothing but read the repository.
|
So unneeded and not even useful. Why is this even merged? |
Fair question. A concrete case: March 2025, tj-actions/changed-files (CVE-2025-30066). The attacker re-pointed the action's existing version tags at code that dumped CI secrets into public build logs, and over 20k repos ran it on their next build without changing a line. Repos pinned by commit SHA were untouched: the tags moved, their refs did not. That pattern matters here because this CI holds real credentials. The Windows job carries the SignPath token that signs the release binaries, and the release workflow publishes what thousands of people download. One re-pointed tag on those paths and you get a tampered fastfetch, signed and shipped under the project's name. The secrets change is the same idea: before, all 17 build jobs received every repo secret; now only the signing job sees the signing token, so a compromised step in one of the BSD build VMs has nothing to steal. Runtime cost is zero, the refs are just frozen, and dependabot keeps bumping them (it already did this morning) |
Drive-by CI hardening in three commits, one logical change each, found with the Plumber CLI (https://github.com/getplumber/plumber) and verified on dev.
Commit 1 pins every action to its commit sha, versions kept as comments. A tag like
@v1or a branch like@masteris a movable pointer: whoever controls the action, or anyone who compromises it, can re-point it and your next run executes their code. The spots that matter here:build-release.ymlcreates the GitHub release with the binaries users download, andbuild-windows-hosts.ymlholds the SignPath token, so a re-pointed tag in those paths could ship a tampered or wrongly signed release. Same pattern as the tj-actions/changed-files incident (CVE-2025-30066). You already pin docker/setup-qemu-action by sha and you bumped action majors by hand before (#808), so this just finishes the job: every sha was resolved from the upstream repo and cross checked against its release tag, and the three@masterrefs (cross-platform-actions, setup-alpine, get-latest-release) are pinned to their latest release tag. The pins stay maintainable with a dependabot config for thegithub-actionsecosystem (the repo has none today, happy to add one here if you want); dependabot and renovate both bump sha pins and keep the version comment in sync.Commit 2 drops
secrets: inheritfrom all 17 reusable workflow calls. The only secret any called workflow reads isSIGNPATH_API_TOKENinbuild-windows-hosts.yml, so that one is declared and passed explicitly and nothing else gets the secret set. A compromised step in one of the BSD build VMs now has no signing token in reach. The secret is optional in the called workflow, so fork and PR runs behave exactly as before (the signing step is already guarded to upstream pushes).Commit 3 scopes the GITHUB_TOKEN per workflow, derived from what each job actually does with it: every build workflow declares
contents: read;build-linux-hosts.ymlkeepssecurity-events: writefor its CodeQL upload while the other callers lose that grant (they never upload scanning results);build-windows-hosts.ymlgetsactions: read, which the SignPath action documents needing to read job details and fetch the unsigned artifact;build-release.ymldeclares thecontents: writeits caller already grants. Runs that execute PR code now hold a read-only token.Two heads up:
owner/action@v1) stop matching once refs are shas and workflows refuse to start. Entries need to beowner/action@*in that case.pozetroninc/github-action-get-latest-releasecan be replaced with a two-linegh apistep; happy to do that here, kept the change minimal for now.I will also open a PR which adds the tool to CI so this does not quietly drift back; that one is a bonus, this PR stands on its own.
Related issue (required for new logos for new distros)
Not a logo PR, no related issue.
Changes
secrets: inheritwith an explicit, declaredSIGNPATH_API_TOKENpass to the one workflow that uses itGITHUB_TOKENpermissions on every workflow, derived from actual token useScreenshots
No visual changes.
Checklist