Skip to content

Harden CI/CD workflow with least-privilege permissions - #2966

Merged
jonathanKingston merged 2 commits into
mainfrom
claude/github-actions-supply-chain-cbejig
Aug 18, 2026
Merged

Harden CI/CD workflow with least-privilege permissions#2966
jonathanKingston merged 2 commits into
mainfrom
claude/github-actions-supply-chain-cbejig

Conversation

@jonathanKingston

@jonathanKingston jonathanKingston commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Description

Refactors the GitHub Actions workflow to follow the principle of least privilege and improve security isolation:

  1. Permission scoping: Changes from permissions: write-all to permissions: {} at the workflow level, with each job explicitly declaring only the permissions it needs (contents: read for build, contents: write + pull-requests: write for publish).

  2. Job separation: Splits the build and publish jobs so that the token with write access to the repository never runs on the same runner as third-party build tooling. Build output is passed via artifact upload/download.

  3. Build hardening:

    • Adds persist-credentials: false to checkout to prevent build scripts from accessing push credentials
    • Installs dependencies with npm ci --ignore-scripts to block lifecycle hooks from untrusted packages
    • Explicitly runs the injected workspace's postinstall script (copy-sjcl) after install
    • Updates cache key to reflect the --ignore-scripts behavior
  4. Artifact handling: Packages build output as a tarball to ensure consistent layout and efficient transfer between jobs.

  5. Branch filtering: Adds dependabot/** branches to the trigger list with a comment explaining the security rationale (dependency bumps are where third-party code enters the repo).

  6. Cleanup job: Adds explicit permissions: contents: write to the cleanup job.

These changes reduce the attack surface by ensuring that if build tooling is compromised, it cannot push code or modify the repository.

Testing Steps

  • CI workflow runs successfully on push to main, releases, and dependabot branches
  • Build job completes and uploads artifact
  • Publish job downloads artifact and creates/pushes build branch as before
  • Cleanup job runs on branch deletion

Checklist

  • This change was covered by a tech design (security hardening)
  • I have tested this change locally (workflow changes require CI validation)

https://claude.ai/code/session_01Y5t5ARFHhq3V9kG7GHs7xM


Note

Medium Risk
Changes CI security boundaries and artifact handoff for pr-releases publishing; behavior should match prior flows but misconfiguration could break PR build branches or leave publish credentials on build runners.

Overview
Hardens build-branch.yml by replacing workflow-wide write-all with per-job permissions, splitting build and publish onto separate runners, and tightening how dependencies and artifacts are handled.

The build job now checks out with persist-credentials: false, installs via npm ci --ignore-scripts (with an explicit copy-sjcl step for injected), bumps the cache key for that install mode, and uploads a single build-output.tar.gz artifact (short retention) instead of pushing from the same job.

A new publish job downloads that tarball, unpacks it, and runs the existing push-to-pr-releases/<branch> and PR annotation logic with only contents: write and pull-requests: write. Dependabot branches are added to branches-ignore so third-party dependency bumps do not run alongside a write-capable publish path. clean_up explicitly requests contents: write.

CONTRIBUTING.md renames the workflow to build-branch.yml, documents the dependabot exclusion, and notes testing Dependabot bumps via normal CI or local builds instead.

Reviewed by Cursor Bugbot for commit 7900d59. Bugbot is set up for automated code reviews on this repo. Configure here.

The Build Release Branch workflow declared `permissions: write-all` and ran
`npm ci` on every push to a non-main branch. Dependabot pushes trigger it
(534 runs to date), so third-party install scripts executed on a runner
holding a repo-write GITHUB_TOKEN — the default read-only boundary for
Dependabot-triggered runs was explicitly overridden.

Three layers, each independently sufficient for the Dependabot path:

- Exclude `dependabot/**` from the push trigger. Dependency bumps do not
  need a build preview branch, and this removes untrusted code from the
  privileged workflow outright.
- Replace `write-all` with per-job least privilege: `contents: read` to
  build, `contents: write` + `pull-requests: write` to publish and clean up.
  Checkout in the build job no longer persists credentials.
- Split build from publish. The build job produces a tarball; the publish
  job downloads it and pushes without ever invoking npm, so the writable
  token and the build tooling never share a runner.

Install is now `npm ci --ignore-scripts`. Only three packages in the tree
declare an install script: `injected` (ours — its `copy-sjcl` postinstall
now runs as an explicit step), `esbuild` (resolves its platform binary at
runtime) and `fsevents` (macOS-only, optional; the runner is ubuntu).
The node_modules cache key gains a `noscripts` segment so this tree is not
restored by workflows that do run install scripts.

Note this bounds token abuse, not artifact tampering: the build job can
still influence its own output. Excluding Dependabot is what addresses the
reported path.

Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y5t5ARFHhq3V9kG7GHs7xM
@jonathanKingston
jonathanKingston requested a review from a team as a code owner August 18, 2026 14:22
@github-actions github-actions Bot added the semver-patch Bug fix / internal — no release needed label Aug 18, 2026
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

[Beta] Generated file diff

Time updated: Tue, 18 Aug 2026 15:23:34 GMT

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Stale comment

Injected PR Evaluation: Web Compatibility & Security

Scope: This PR modifies only .github/workflows/build-branch.yml (CI/CD supply-chain hardening). No files under injected/, messaging/, or special-pages/ runtime paths are changed.


Web Compatibility Assessment

No findings.

This workflow change does not alter injected script bundles, feature initialization, API overrides, DOM interaction, or platform entry points. There is zero user-facing web compatibility surface in this diff.


Security Assessment

No injected-runtime findings.

The diff does not touch captured-globals.js, messaging transports, the message bridge, wrapper-utils.js, DDGProxy, feature code, or remote config. None of the injected-script threat models apply.

CI supply-chain observations (informational, not injected-runtime risks):

File Lines Severity Note
.github/workflows/build-branch.yml 15–16, 23–24, 91–93, 298–299 info Least-privilege permissions: {} default with per-job opt-in (contents: read for build, contents: write + pull-requests: write for publish/clean_up) is a solid hardening pattern.
.github/workflows/build-branch.yml 29–32 info persist-credentials: false on the build job correctly prevents a write-capable token from persisting while third-party tooling runs.
.github/workflows/build-branch.yml 51–63 info npm ci --ignore-scripts with an explicit copy-sjcl step blocks arbitrary dependency postinstall hooks. The three hasInstallScript packages (injected, esbuild, fsevents) are accounted for; esbuild resolves its platform binary at runtime on Linux.
.github/workflows/build-branch.yml 86–107 info Splitting build and publish jobs so contents: write never shares a runner with npm ci/build tooling is the core security win of this PR. The publish job only unpacks a workflow artifact and runs git/gh-script — no dependency install.
.github/workflows/build-branch.yml 9–12 info Ignoring dependabot/** prevents dependency-bump branches from triggering a job that eventually holds write credentials. Trade-off: Dependabot PRs will not receive pr-releases/ build branches until merged/rebased onto a non-ignored branch.
.github/workflows/build-branch.yml 49, 59–63 info Cache key bump (noscripts) correctly invalidates stale node_modules caches from the pre---ignore-scripts era. On cache hit, copy-sjcl is skipped but the committed injected/lib/sjcl.js satisfies the build — same behavior as the prior workflow on cache hit.

Risk Level

Low Risk — CI workflow hardening only; no injected runtime code, API overrides, messaging, or security-sensitive browser-facing changes.


Recommendations

  1. No injected-runtime action required. Safe to merge from a C-S-S web-compat/security perspective.
  2. (info) Consider documenting in CONTRIBUTING.md that Dependabot PRs intentionally skip pr-releases/ build branches due to the supply-chain separation, so reviewers know to test Dependabot bumps via the main CI workflow or local builds.
  3. (info) If injected/scripts/generateSJCL.js changes without a lockfile bump, cache-hit builds will use the committed lib/sjcl.js rather than regenerating — pre-existing behavior, but worth keeping in mind when reviewing SJCL-related changes.
Open in Web View Automation 

Sent by Cursor Automation: Web compat and sec

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 9e980e8. Configure here.

Comment thread .github/workflows/build-branch.yml
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Cursor review was not successful.

This PR requires a manual review and approval from a member of one of the following teams:

  • @duckduckgo/content-scope-scripts-owners
  • @duckduckgo/apple-devs
  • @duckduckgo/android-devs
  • @duckduckgo/team-windows-development
  • @duckduckgo/extension-owners
  • @duckduckgo/config-aor
  • @duckduckgo/breakage-aor
  • @duckduckgo/breakage

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Build Branch

Branch pr-releases/claude/github-actions-supply-chain-cbejig
Commit fb6bc68a11
Updated August 18, 2026 at 3:23:17 PM UTC

Static preview entry points

QR codes (mobile preview)
Entry point QR code
Docs QR for docs preview
Static pages QR for static pages preview
Integration pages QR for integration pages preview

Integration commands

npm (Android / Extension):

npm i github:duckduckgo/content-scope-scripts#pr-releases/claude/github-actions-supply-chain-cbejig

Swift Package Manager (Apple):

.package(url: "https://github.com/duckduckgo/content-scope-scripts.git", branch: "pr-releases/claude/github-actions-supply-chain-cbejig")

git submodule (Windows):

git -C submodules/content-scope-scripts fetch origin pr-releases/claude/github-actions-supply-chain-cbejig
git -C submodules/content-scope-scripts checkout origin/pr-releases/claude/github-actions-supply-chain-cbejig
Pin to exact commit

npm (Android / Extension):

npm i github:duckduckgo/content-scope-scripts#fb6bc68a11202356fa4d15063894c8e66d4fa526

Swift Package Manager (Apple):

.package(url: "https://github.com/duckduckgo/content-scope-scripts.git", revision: "fb6bc68a11202356fa4d15063894c8e66d4fa526")

git submodule (Windows):

git -C submodules/content-scope-scripts fetch origin pr-releases/claude/github-actions-supply-chain-cbejig
git -C submodules/content-scope-scripts checkout fb6bc68a11202356fa4d15063894c8e66d4fa526

The workflow change made the CONTRIBUTING.md exclusion list inaccurate.
Add `dependabot/*` and explain why those branches are skipped.

Also corrects two references to `build-pr.yml`, which does not exist —
the workflow is `build-branch.yml`.

Co-Authored-By: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01Y5t5ARFHhq3V9kG7GHs7xM

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Injected PR Evaluation: Web Compatibility & Security

Assessed at: 7900d591 (synchronize) — delta since prior review is CONTRIBUTING.md documentation only.


Web Compatibility Assessment

No findings.

This PR touches only .github/workflows/build-branch.yml and CONTRIBUTING.md. There are no changes under injected/, messaging/, special-pages/, or platform entry points. None of the web-compatibility evaluation categories (API surface fidelity, prototype chain integrity, DOM interaction, timing, platform-specific breakage, third-party script compatibility) apply.


Security Assessment

No injected-runtime security findings.

The changed files are CI/CD infrastructure and contributor documentation — outside the page-injection threat model. The workflow changes are supply-chain hardening (positive security posture), not regressions:

File Lines Severity Notes
.github/workflows/build-branch.yml 15–16, 23–24, 29–32 info Workflow-level permissions: {} with per-job least-privilege (contents: read for build, contents: write + pull-requests: write for publish/clean_up).
.github/workflows/build-branch.yml 86–107 info Build/publish job split via artifact handoff — publish runner never executes third-party npm ci tooling, isolating the contents: write token.
.github/workflows/build-branch.yml 29–32, 57 info persist-credentials: false on build checkout + npm ci --ignore-scripts blocks lifecycle-hook execution during dependency install.
.github/workflows/build-branch.yml 59–63 info Explicit npm run copy-sjcl -w injected re-runs the one required postinstall script after --ignore-scripts.
.github/workflows/build-branch.yml 9–12 info dependabot/** excluded from push trigger — prevents auto-publishing build branches from dependency-bump PRs that introduce third-party code.
CONTRIBUTING.md 116, 139, 149 info Documents the Dependabot exclusion and corrects workflow filename reference (build-branch.yml). No runtime impact.

Info — cache key bump (noscripts): Line 49 invalidates cached node_modules from pre---ignore-scripts installs. Correct hygiene; no security concern.

Info — SJCL on cache hit: Generate SJCL bundle (lines 59–63) is gated on cache miss. A warm cache from a prior successful run already contains the generated bundle; no gap identified.


Risk Level

Low Risk — CI/CD workflow hardening and contributor documentation only; zero injected-runtime, messaging, or special-pages code changes.


Recommendations

No blocking items. Optional follow-ups (CI hygiene, not injected-security):

  1. Verify end-to-end — Confirm a non-Dependabot feature-branch push still produces a pr-releases/<branch> artifact and PR comment after the build/publish split.
  2. Monitor first cache miss — After merge, watch one cold-cache run to confirm copy-sjcl + npm run build succeed under --ignore-scripts.
Open in Web View Automation 

Sent by Cursor Automation: Web compat and sec

@mgurgel mgurgel left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

👏

@jonathanKingston
jonathanKingston added this pull request to the merge queue Aug 18, 2026
Merged via the queue into main with commit 99aca2e Aug 18, 2026
45 checks passed
@jonathanKingston
jonathanKingston deleted the claude/github-actions-supply-chain-cbejig branch August 18, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver-patch Bug fix / internal — no release needed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants