Skip to content

security: harden CI/CD pipeline against supply chain attacks#20

Open
ajag408 wants to merge 6 commits into
mainfrom
security/eng-2688-ci-supply-chain-hardening
Open

security: harden CI/CD pipeline against supply chain attacks#20
ajag408 wants to merge 6 commits into
mainfrom
security/eng-2688-ci-supply-chain-hardening

Conversation

@ajag408
Copy link
Copy Markdown
Contributor

@ajag408 ajag408 commented May 14, 2026

Motivation

The TanStack/Mini Shai-Hulud supply chain attack (May 2026) compromised 169 npm packages by poisoning GitHub Actions caches via PR CI runs, then publishing malicious packages through trusted release workflows.

Shield's build-binaries job shared a pnpm store cache between PR CI runs and release builds — the same vector TanStack was exploited through. Since Trust will consume the SEA binary directly, the entire build pipeline must be hardened before the next release tag.

The monorepo already has most of these layers (Socket Firewall, harden-runner, persist-credentials). This PR brings Shield to parity and adds install-time controls the monorepo should also adopt.

Changes

Install-time hardening

Change File Why
Remove enable-pre-post-scripts=true .npmrc Today every dependency can run arbitrary code on install. This is the exact mechanism Mini Shai-Hulud uses (preinstall, prepare hooks). Removing it blocks all lifecycle scripts by default.
Add onlyBuiltDependencies: ["esbuild"] package.json Allowlist for packages permitted to run postinstall. Only esbuild needs it (downloads platform binary). Everything else blocked.
Add minimum-release-age=4320 .npmrc 3-day publish cooldown. pnpm refuses package versions published less than 3 days ago. The TanStack malicious versions were all caught within hours — this would have blocked them.

Release-path hardening

Change File Why
Remove actions/cache from build-binaries release.yml Closes TanStack-class cache poisoning on the binary build path. publish job was already cache-free. ~30s slower, eliminates shared cache as attack vector.
Add NPM_CONFIG_PROVENANCE=true on publish release.yml Attaches SLSA provenance to the npm package. Matches the attest-build-provenance already done for binaries. Consumers can verify the package was built by our GitHub Actions workflow.

Detection

Change File(s) Why
Add step-security/harden-runner (audit mode) ci.yml + release.yml Egress auditing — logs all outbound network connections in every job. Catches exfiltration attempts. SHA-pinned to match monorepo (v2.16.1).
Add persist-credentials: false on all checkouts ci.yml + release.yml Prevents GITHUB_TOKEN from being persisted to .git/config. Matches monorepo.
Add Socket Firewall (socketdev/action) on all installs ci.yml + release.yml Install-time malware interception — checks every package against Socket's database before lifecycle scripts execute. Matches monorepo. Requires SOCKET_SECURITY_API_KEY repo secret (see below).
Add actions/dependency-review-action on PRs ci.yml Flags new dependencies or version changes that introduce known vulnerabilities. Runs on pull_request only.

Governance

Change File Why
Expand CODEOWNERS .github/CODEOWNERS Adds .npmrc, package.json, pnpm-lock.yaml to require review for supply-chain-critical files.

What's NOT changed

  • No validator logic changes
  • No package output changes (npm tarball identical)
  • All existing action SHA pins preserved (already good)
  • pnpm version stays at 10.12.2

Admin actions required

  1. Before CI can pass: set SOCKET_SECURITY_API_KEY as a repo secret (Settings → Secrets → Actions). Get the key from the monorepo's secrets or Socket dashboard.
  2. After merge: set branch protection on main and tag protection on v* (see Slack thread / PR comments for details).
  3. After merge: purge all GitHub Actions caches before tagging the next release.

Test plan

  • CI passes (harden-runner, Socket Firewall, dependency-review all active)
  • harden-runner egress logs visible in Actions run
  • Socket Firewall wraps install step (sfw visible in logs)
  • esbuild postinstall still runs (build succeeds despite onlyBuiltDependencies)
  • After merge: purge all caches, tag release, confirm build-binaries runs clean without cache
  • After merge: confirm npm package has provenance attached

Follow-up (separate PRs / settings)

  • Branch protection on main (require PR review + CI, no force-push)
  • Tag protection on v*
  • After 2-4 weeks of harden-runner audit logs: build egress allowlist, flip release.yml to egress-policy: block

Summary by CodeRabbit

  • Chores
    • Expanded supply-chain ownership for critical configuration files to improve accountability.
    • Hardened CI workflows with runner/network protections, socket firewall, non-persistent checkouts, Corepack-based package manager setup, and a new dependency-review check for PRs.
    • Made dependency audit steps non-fatal where appropriate and simplified caching/installation steps.
    • Strengthened release pipeline with provenance-enabled publishing and pinned toolchain behavior.
    • Adjusted package/npm settings to control dependency builds and release timing.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 14, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds CODEOWNERS for manifests, updates npm/pnpm configs, and hardens CI and release workflows by disabling persisted checkout credentials, using Corepack + Socket Firewall for installs, making critical audit non-fatal, and adding a PR-only dependency-review job.

Changes

Supply Chain Security Hardening

Layer / File(s) Summary
Code Ownership and Package Configuration
.github/CODEOWNERS, .npmrc, package.json
Adds owners for .npmrc, package.json, and pnpm-lock.yaml; removes enable-pre-post-scripts and sets minimum-release-age=4320 in .npmrc; adds pnpm.onlyBuiltDependencies = ["esbuild"] to package.json.
CI Workflow Hardening
.github/workflows/ci.yml
test and security jobs add step-security/harden-runner, set persist-credentials: false for checkout, install pnpm via Corepack, add Socket Firewall steps, switch installs to sfw pnpm install --frozen-lockfile, change Codecov filefiles, make critical pnpm audit non-fatal, and add a PR-only dependency-review job.
Release Workflow Hardening
.github/workflows/release.yml
Adds runner hardening and non-persistent checkout across release jobs, replaces pnpm setup with Corepack + Socket Firewall and sfw pnpm install --frozen-lockfile, removes pnpm cache/store logic in build-binaries, pins npm toolchain for publish, and sets NPM_CONFIG_PROVENANCE: "true" for publish.

Sequence Diagram

sequenceDiagram
  participant PR as "Pull Request"
  participant CI as "CI Job"
  participant SFW as "Socket Firewall"
  participant Corepack
  participant pnpm
  participant Registry as "npm Registry"
  PR->>CI: trigger test/security/publish job
  CI->>Corepack: corepack prepare pnpm
  CI->>SFW: setup socket firewall
  CI->>SFW: sfw pnpm install --frozen-lockfile
  SFW->>pnpm: allow install
  pnpm->>Registry: fetch packages / publish artifacts
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • stakekit/shield#10: Modifies the same CI and release workflow files with similar hardening and pnpm/checkout changes.

Suggested reviewers

  • petar-omni
  • Philippoes

Poem

🐰 I hopped through manifests, marked each tiny file,

Firewalls hum softly, keeping installs in style.
Corepack wakes pnpm, locks the dependency door,
Audits now gentle, provenance set before.
Hop, review, publish — safe trails forevermore.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of the PR: hardening the CI/CD pipeline against supply chain attacks. It is concise, specific, and directly reflects the primary change documented in the PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch security/eng-2688-ci-supply-chain-hardening

Comment @coderabbitai help to get the list of available commands and usage tips.

@ajag408 ajag408 requested review from aditya172926 and auralshin May 14, 2026 06:54
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 42-48: The Socket Firewall steps ("Set up Socket Firewall" action
socketdev/action) and the "Install dependencies" run step must explicitly expose
the API key by adding the secret as an environment variable (e.g., add an env
mapping SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_SECURITY_API_KEY }} on the
Socket step and the run step that invokes `sfw pnpm install`) so the action and
`sfw` CLI can read it at runtime; replicate the same change for the security job
steps that run the firewall and `sfw` install. Ensure each step that invokes the
socket action or `sfw` commands has the env entry so the secret is available
during execution (and consider using workflow-level or job-level env if multiple
steps need it).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 97609dfa-cad3-4dfe-959f-c25b5110aee0

📥 Commits

Reviewing files that changed from the base of the PR and between 46094dc and 791b5e9.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/release.yml

Comment thread .github/workflows/ci.yml
jdomingos
jdomingos previously approved these changes May 19, 2026
Copy link
Copy Markdown

@raiseerco raiseerco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

comment! all other lgtm

Comment thread .github/workflows/ci.yml Outdated
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
npm install -g corepack@latest
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe there is still a hardening gap here: npm install -g corepack@latest performs a registry fetch/execution before socketdev/action, so the pipeline is not protected from the first external package step...in addition, corepack prepare pnpm@10.12.2 --activate can also require fetching the package manager before the firewall is active
could we keep pnpm but move its bootstrap to a pinned path behind the firewall?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. The @latest on corepack was an unpinned fetch before the firewall — fixed in the latest commit, now pinned to corepack@0.35.0. Also pinned npm@11.15.0 in the publish job for the same reason.

Worth noting this is a fundamental bootstrapping constraint: Socket Firewall wraps pnpm install (dependency resolution), not npm install -g or corepack prepare. I don't think moving the bootstrap behind the firewall is possible, I don't think sfw doesn't intercept those commands. The monorepo has the same ordering (corepack before Socket in initial_setup/action.yml).

The remaining surface after pinning:

  • corepack is Node.js-maintained (high-trust, high-scrutiny)
  • corepack prepare validates pnpm's signature against known keys before activation
  • All dependency installation (sfw pnpm install --frozen-lockfile) is fully protected

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/release.yml (1)

133-136: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Critical: npm publish step missing npm authentication token

  • .github/workflows/release.yml publish step sets only NPM_CONFIG_PROVENANCE and does not pass any npm auth token (no NODE_AUTH_TOKEN/NPM_TOKEN secret is referenced anywhere in workflows).
  • registry-url alone is insufficient—pnpm publish needs an auth token env/secret to publish.
  • Fix: add the repo’s npm publish token to the publish step (typically env.NODE_AUTH_TOKEN: ${{ secrets.<npm token secret> }}).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 133 - 136, The Publish to NPM
step currently runs "pnpm publish --access public --no-git-checks" and only sets
NPM_CONFIG_PROVENANCE; add the npm authentication token to the step by setting
the environment variable used by pnpm (e.g., NODE_AUTH_TOKEN or NPM_TOKEN) to
the repository secret (secrets.<YOUR_NPM_SECRET>) so pnpm can authenticate;
update the publish step's env to include NODE_AUTH_TOKEN: ${{ secrets.<npm token
secret> }} alongside NPM_CONFIG_PROVENANCE to enable successful publishing.
🧹 Nitpick comments (2)
.github/workflows/ci.yml (1)

36-48: ⚖️ Poor tradeoff

Pre-firewall fetch gap remains unaddressed.

As noted in a previous review, npm install -g corepack@0.35.0 and corepack prepare pnpm@10.12.2 --activate execute registry fetches before Socket Firewall is active. While pinning versions mitigates risk, it doesn't eliminate the window.

Consider either:

  1. Move Socket Firewall setup earlier (after checkout, before any npm/corepack commands)
  2. Use a pre-cached corepack/pnpm in the runner image
  3. Accept the risk given pinned versions and document this gap in the PR
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 36 - 48, The workflow currently runs
the "Install pnpm" step (commands npm install -g corepack@0.35.0 and corepack
prepare pnpm@10.12.2 --activate) before the "Set up Socket Firewall" action,
leaving a pre-firewall registry fetch window; to fix, move the Socket Firewall
step so it runs immediately after checkout and before any npm/corepack commands
(i.e., place the uses: socketdev/action@... step earlier), or alternatively
document/accept the residual risk in the PR and add a comment explaining why
pinned versions are considered sufficient; ensure you update or keep the step
names "Install pnpm" and "Set up Socket Firewall" so reviewers can verify the
change.
.github/workflows/release.yml (1)

114-115: ⚖️ Poor tradeoff

Additional pre-firewall registry fetch.

npm install -g npm@11.15.0 executes before Socket Firewall is active (line 117), adding another unprotected registry fetch to the publish job beyond the corepack installation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 114 - 115, The "Update npm" step
(named "Update npm" which runs npm install -g npm@11.15.0) performs an
unprotected registry fetch before the Socket Firewall step is activated; move or
reorder this step so it runs after the Socket Firewall activation step in the
publish job (or remove it if corepack already supplies the needed npm), ensuring
the "Update npm" step executes only once the firewall is active to avoid extra
unprotected registry access.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In @.github/workflows/release.yml:
- Around line 133-136: The Publish to NPM step currently runs "pnpm publish
--access public --no-git-checks" and only sets NPM_CONFIG_PROVENANCE; add the
npm authentication token to the step by setting the environment variable used by
pnpm (e.g., NODE_AUTH_TOKEN or NPM_TOKEN) to the repository secret
(secrets.<YOUR_NPM_SECRET>) so pnpm can authenticate; update the publish step's
env to include NODE_AUTH_TOKEN: ${{ secrets.<npm token secret> }} alongside
NPM_CONFIG_PROVENANCE to enable successful publishing.

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 36-48: The workflow currently runs the "Install pnpm" step
(commands npm install -g corepack@0.35.0 and corepack prepare pnpm@10.12.2
--activate) before the "Set up Socket Firewall" action, leaving a pre-firewall
registry fetch window; to fix, move the Socket Firewall step so it runs
immediately after checkout and before any npm/corepack commands (i.e., place the
uses: socketdev/action@... step earlier), or alternatively document/accept the
residual risk in the PR and add a comment explaining why pinned versions are
considered sufficient; ensure you update or keep the step names "Install pnpm"
and "Set up Socket Firewall" so reviewers can verify the change.

In @.github/workflows/release.yml:
- Around line 114-115: The "Update npm" step (named "Update npm" which runs npm
install -g npm@11.15.0) performs an unprotected registry fetch before the Socket
Firewall step is activated; move or reorder this step so it runs after the
Socket Firewall activation step in the publish job (or remove it if corepack
already supplies the needed npm), ensuring the "Update npm" step executes only
once the firewall is active to avoid extra unprotected registry access.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ace7a99b-3c93-42e5-97ce-2a1fdc206ad4

📥 Commits

Reviewing files that changed from the base of the PR and between 48c9fef and 1fa5f8c.

📒 Files selected for processing (2)
  • .github/workflows/ci.yml
  • .github/workflows/release.yml

@ajag408 ajag408 requested review from jdomingos and raiseerco May 22, 2026 06:49
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.

3 participants