Skip to content

ci: harden GitHub Actions supply chain (pin actions, drop unused setup-deno, least-privilege permissions)#222

Merged
chhoumann merged 1 commit into
masterfrom
chhoumann/deepsec-ci-hardening
Jun 29, 2026
Merged

ci: harden GitHub Actions supply chain (pin actions, drop unused setup-deno, least-privilege permissions)#222
chhoumann merged 1 commit into
masterfrom
chhoumann/deepsec-ci-hardening

Conversation

@chhoumann

Copy link
Copy Markdown
Owner

Hardens all GitHub Actions workflows against the supply-chain risk class flagged by deepsec: an unused, unpinned third-party action in the privileged release pipeline, plus unpinned actions and missing least-privilege permissions across the repo.

What changed

Remove the unused, unpinned third-party action

  • Dropped denolib/setup-deno@v2 from release.yml and test.yml. Deno is used nowhere in the repository (a word-boundary \bdeno\b search across the tree returns zero real hits, no deno.json/deno.lock/import map). It was dead weight and pure attack surface - denolib is the older project superseded by canonical denoland/setup-deno, and v2 is a mutable major tag, so a tag re-point or org compromise would have injected code into CI. In release.yml it ran before npm install/npm run build/Release with contents/issues/pull-requests/id-token/attestations: write and GITHUB_TOKEN/NPM_TOKEN in scope.

Pin every remaining action to a full commit SHA (with a # vX.Y.Z comment for readability), so a mutable tag can no longer be silently re-pointed:

  • actions/checkout -> 34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
  • actions/setup-node -> 49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
  • actions/attest-build-provenance -> e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
  • actions/setup-python -> a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
  • test.yml checkout was @v2 (long-EOL Node 16 runtime) and is upgraded to the v4.3.1 SHA above.

Each SHA was verified against the GitHub API to resolve to its claimed tag and to point at a commit object.

Add least-privilege permissions: blocks

  • release.yml: top-level default-deny permissions: {}. The release job keeps its existing explicit job-level write grants (which override the top-level for that job), so the semantic-release push, release/comment, and attestation steps are unaffected.
  • test.yml: permissions: contents: read (checkout + install + lint/format/typecheck/build/test are read-only; the npm cache does not need actions: write).
  • documentation.yml: permissions: contents: read (checkout + setup-python + mkdocs build; there is no deploy/push step in this workflow - docs:deploy/wrangler is a separate, manually-run script).

Why these choices

  • I pinned the current major of each action to its latest patch SHA rather than bumping to newer majors (checkout v7, setup-node v6, etc.). Pinning is the actual supply-chain fix; untested major bumps in the privileged release pipeline would add unrelated breakage risk.
  • I did not add persist-credentials: false to the release checkout. @semantic-release/git pushes the version-bump commit/tag, and the persisted token authorizes that push. semantic-release does fall back to the GITHUB_TOKEN-authenticated URL when persisted creds are absent, so it is likely safe, but it touches the production release push path and cannot be validated without a live release, so I kept it out of scope. npm install -> npm ci is a similar pre-existing, separately-validatable hardening left for a follow-up.

Validation

  • npm run lint, format:check, typecheck, build, and test (769 tests) all pass on Node 22.
  • All three workflows parse as valid YAML; no tag/branch action refs remain (every uses: is a 40-hex SHA).
  • Adversarially self-reviewed (subagent): pinning complete and SHAs verified, Deno removal safe, each permissions block sufficient and minimal, and the release job retains exactly the write scopes it needs via job-level override.

Resolves deepsec findings other-ci-supply-chain (0995a86e73, e6c687034d).

Remove the unused, unpinned denolib/setup-deno@v2 third-party action from
the release and test workflows (Deno is used nowhere in the repo), pin
every remaining action to a full commit SHA, and add least-privilege
permissions blocks.

- release.yml: drop the Setup Deno step; pin checkout, setup-node, and
  attest-build-provenance to SHAs; add a top-level default-deny
  `permissions: {}` while the release job keeps its explicit write grants.
- test.yml: drop the Setup Deno step; upgrade checkout v2 (EOL Node 16) to
  v4 pinned to a SHA; pin setup-node; add `permissions: contents: read`.
- documentation.yml: pin checkout and setup-python to SHAs; add
  `permissions: contents: read`.

Resolves deepsec findings other-ci-supply-chain (0995a86e73, e6c687034d).
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying podnotes with  Cloudflare Pages  Cloudflare Pages

Latest commit: bc910a6
Status: ✅  Deploy successful!
Preview URL: https://409fcbbe.podnotes.pages.dev
Branch Preview URL: https://chhoumann-deepsec-ci-hardeni.podnotes.pages.dev

View logs

@chhoumann chhoumann marked this pull request as ready for review June 29, 2026 07:05
@chhoumann chhoumann merged commit 989e8bb into master Jun 29, 2026
3 checks passed
@chhoumann chhoumann deleted the chhoumann/deepsec-ci-hardening branch June 29, 2026 07:07
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 2.17.3 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant