Skip to content

security(ci): enforce SHA-pinned and allowlisted GitHub Actions - #562

Open
sulaimonifeoluwa4-blip wants to merge 1 commit into
Sub-Rosa-Issue:mainfrom
sulaimonifeoluwa4-blip:security/489-actions-sha-pins-allowlist
Open

sulaimonifeoluwa4-blip wants to merge 1 commit into
Sub-Rosa-Issue:mainfrom
sulaimonifeoluwa4-blip:security/489-actions-sha-pins-allowlist

Conversation

@sulaimonifeoluwa4-blip

@sulaimonifeoluwa4-blip sulaimonifeoluwa4-blip commented Oct 23, 2026 •

Copy link
Copy Markdown

Summary

Closes #489.

Every external GitHub Action used by .github/workflows/** is now referenced by a full-length commit SHA, recorded in a machine-readable allowlist, and verified by a blocking CI job plus tests. All workflows keep permissions: contents: read; no token scope was added.

Issue #489 expected the file-level pins from #481 to be merged first. Those pins are not on main (all workflows still used actions/checkout@v4, actions/setup-node@v4, pnpm/action-setup@v4, Swatinem/rust-cache@v2, dtolnay/rust-toolchain@stable), so this PR includes the pinning itself and then enforces it. The repository settings requested by the issue cannot be stored in git — the required settings and the final allowlist are documented in docs/CI.md, and the new check enforces the same rules at review time so the policy cannot drift between settings and files.

Changes

File Change
.github/workflows/bindings-check.yml 5 actions pinned; explicit toolchain: stable
.github/workflows/contract.yml 4 actions pinned; explicit toolchain: stable
.github/workflows/coverage.yml 3 actions pinned
.github/workflows/docs-links.yml 2 actions pinned
.github/workflows/env-examples.yml 2 actions pinned
.github/workflows/line-lengths.yml 3 actions pinned
.github/workflows/web.yml 3 actions pinned
.github/actions-allowlist.json new — reviewed action → SHA → version allowlist
scripts/check-actions-policy.mjs new — dependency-free policy checker
scripts/check-actions-policy.test.mjs new — 13 tests, including docs/allowlist sync
.github/workflows/actions-policy.yml new — runs the checker and its tests in CI
package.json adds actions:check and actions:test script aliases
docs/CI.md documents the policy, the allowlist, the update procedure, and the maintainer settings

No dependencies, lockfile entries, lifecycle scripts, binaries, or secret references were added.

Final allowlist

Each SHA is exactly the commit the previously used mutable ref resolved to when this PR was prepared (git ls-remote, 2026-10-23). No action code changes — only the reference stops being mutable.

Action Before After (pin) Resolved ref
actions/checkout @v4 11d5960a326750d5838078e36cf38b85af677262 v4.4.0
actions/setup-node @v4 49933ea5288caeca8642d1e84afbd3f7d6820020 v4.4.0
pnpm/action-setup @v4 b906affcce14559ad1aafd4ab0e942779e9f58b1 v4.3.0
dtolnay/rust-toolchain @stable b725eda525362e7cf34af9a7243cac1e3f01a7ec stable branch head
Swatinem/rust-cache @v2 d1527c8f67ef9af357290475d4af4b0fab8f12e0 v2.9.3

dtolnay/rust-toolchain caveat: the action derives the toolchain from the ref it is called with (@stable). Once that ref is a SHA, the toolchain name is gone, so both usages now pass toolchain: stable explicitly. Behavior is unchanged.

Enforcement added

scripts/check-actions-policy.mjs (no dependencies, runs on plain Node 22) fails when:

  • an external action is not pinned to a full-length (40-char, lowercase) commit SHA — tags/branches such as @v4 or @stable are rejected;
  • a pinned action is not in .github/actions-allowlist.json (even when the SHA is valid);
  • an allowlisted action is pinned to a SHA that was not reviewed for it;
  • an allowlist entry is unused — keeps the allowlist from silently widening beyond what is actually reviewed;
  • a docker:// action is used (cannot be pinned by commit SHA);
  • a permissions: block grants write at workflow or job level (inline write-all/{ contents: write } also rejected).

Local composite actions (uses: ./...) are part of this repository and remain allowed. Success output: ✅ 5 allowlisted action pin(s) across 8 workflow(s) verified.

scripts/check-actions-policy.test.mjs covers each of those rules, local actions, commented-out entries, .yaml files, missing/malformed allowlists, and asserts that docs/CI.md stays in sync with the reviewed allowlist.

.github/workflows/actions-policy.yml runs node --test + the checker on PRs and pushes that touch workflows, the allowlist, the checker, or package.json.

Verification

Run locally on this branch:

$ node --test scripts/check-actions-policy.test.mjs
ℹ tests 13
ℹ pass 13
ℹ fail 0

$ pnpm actions:check
✅ 5 allowlisted action pin(s) across 8 workflow(s) verified.

$ pnpm actions:test
ℹ tests 13 / pass 13 / fail 0
  • YAML validity: all 8 workflow files (7 existing + 1 new) parse successfully with js-yaml; each pin was checked against the upstream repo with git ls-remote, and the action metadata at each pinned SHA was fetched to confirm it exists.
  • Pins exercised on a real runner: all eight workflows are recognized by Actions on this PR. Contributed workflow runs require maintainer approval, so the queued jobs report action_required rather than running here; the two path-scoped jobs that do run unapproved (Docs link check, Environment template check) passed. A throwaway fork-only run of actions/checkout@11d5960…, pnpm/action-setup@b906aff…, dtolnay/rust-toolchain@b725eda… (with toolchain: stable), and Swatinem/rust-cache@d1527c8… completed successfully on ubuntu-latest, confirming every pin executes.
  • Existing gates re-run and still green on this branch: scripts/check-bindings-cli.test.mjs (3/3), scripts/web-ci.test.mjs, scripts/check-env-examples.mjs + tests (6/6), scripts/check-deploy-docs.mjs, scripts/check-links.js (43 local links, includes docs/CI.md).
  • pnpm line-lengths:check and scripts/check-line-lengths.test.mjs fail on this contributor's Windows machine for pre-existing, unrelated reasons: the test harness builds paths with new URL(...).pathname (C:\... becomes /C:/...), and the checker compares the allowlist entry apps/web/package.json against a \-separated path, so an already-allowlisted line is reported. Neither file is touched here and both pass on the Ubuntu runners CI uses; I left them alone to keep this PR scoped.
  • The full workflow suites (web, contract, coverage, bindings) exercise unchanged application/contract code; their configurations are unmodified apart from the pins, and this PR's own Actions policy job verifies the pins.

Repository settings required (maintainers, not representable in git)

In Settings → Actions → General:

  1. Actions permissions: Allow select actions and reusable workflows.
  2. Allow GitHub-owned actions, verified Marketplace actions, and the three reviewed third-party actions: pnpm/action-setup, dtolnay/rust-toolchain, Swatinem/rust-cache. Nothing else is needed by the current workflows.
  3. Enable Require actions to be pinned to a full-length commit SHA.

With those settings, every action used by all eight workflows is inside the allowed set; docs/CI.md records the same list so future changes are reviewable against it.

Supply-chain checklist

  • No package scripts, dependencies, or lifecycle hooks added (package.json only gains two script aliases); no lockfile changes.
  • Workflow changes are limited to SHA pins and the explicit toolchain: stable input; triggers and contents: read permissions are untouched.
  • No binaries, fonts, or encoded payloads.
  • No secrets or credentials added.

Closes #489

…llowlist

Tags such as actions/checkout@v4 and dtolnay/rust-toolchain@stable are mutable,
so a compromised or retargeted upstream ref can run unreviewed code with the
repository token. Pin every external action to a full-length commit SHA, record
the reviewed pins in .github/actions-allowlist.json, and add a dependency-free
checker plus CI workflow so an unpinned, unknown, or unused action fails review.
Token permissions stay read-only and the checker rejects new write scopes.

Closes Sub-Rosa-Issue#489

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
@karagozemin

Copy link
Copy Markdown
Collaborator

I reviewed the diff and did not find a malicious payload, but this branch now conflicts with current main after related changes landed. Please rebase on current main, preserve the already-merged security and CI gates, rerun the relevant tests, and update this PR; it is not safe to merge the stale conflict state.

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.

security(high/easy): enforce SHA-pinned and allowlisted GitHub Actions

3 participants