Skip to content

Add least-privilege permissions blocks to all GitHub workflows #5915

@rtibbles

Description

@rtibbles

This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.

Overview

Add least-privilege top-level permissions: blocks to all GitHub workflows in .github/workflows/ that currently lack one. Reduces the blast radius if any workflow gets compromised by a malicious action, malicious dependency, or PR-controlled code by ensuring each workflow's GITHUB_TOKEN only grants the operations that workflow actually performs.

Complexity: Medium
Target branch: unstable

Context

GitHub workflows that don't declare a top-level permissions: block inherit the repository's default GITHUB_TOKEN permission set, which is typically read-all (or write-all on legacy settings). That means every workflow — even simple test runners — implicitly has the ability to read or write to anything the token covers.

An audit of the 14 workflow files in .github/workflows/ produced a proposed minimal-permission block for each, listed in Acceptance Criteria below. The proposal was generated by reading each file and matching the operations it performs (checkout, container push, PR creation, etc.) to the corresponding GitHub permission scopes. CodeQL reports 14 outstanding actions/missing-workflow-permissions alerts across these files.

The Change

For each workflow file listed, add a top-level permissions: block matching the proposed scopes. Where the workflow uses a separate bot token (secrets.LE_BOT_PRIVATE_KEY via actions/create-github-app-token, or peter-evans/create-pull-request with a generated app token) for any write operation, the GITHUB_TOKEN itself can stay locked down — only what the workflow's own steps need.

A few cases worth calling out:

  • Reusable-workflow callers (the call-* shims plus community-contribution-labeling.yml and unassign-inactive.yaml): when a job uses uses: learningequality/.github/..., the called workflow's permissions are bounded by the caller's. The top-level block on the caller must allow at least as much as the callee needs. All these delegate write operations to the LE bot token, so contents: read is sufficient.
  • pull_request_target workflows (call-contributor-pr-reply.yml, call-pull-request-target.yml, call-update-pr-spreadsheet.yml): privileged trigger. Bot-token-based work is preferred over expanding GITHUB_TOKEN permissions.
  • containerbuild.yml: pushes images to ghcr.io on non-PR runs (logs in via ${{ secrets.GITHUB_TOKEN }}). Requires packages: write at the top level so the push step has the scope it needs.

Out of Scope

  • Refactoring workflow logic itself.
  • Migrating off third-party actions (e.g., peter-evans/create-pull-request, fkirc/skip-duplicate-actions).
  • The internal behaviour of the shared learningequality/.github workflows called by the call-* shims.
  • Workflows added since this audit was conducted — they should be reviewed separately.

Acceptance Criteria

Each workflow file below has a top-level permissions: block matching (or strictly tighter than) the audit recommendation. If the recommendation looks wrong on close reading, prefer the tighter version and document why in the commit.

Delegated call-* shims and bot-token delegators

  • call-contributor-issue-comment.ymlcontents: read
  • call-contributor-pr-reply.ymlcontents: read
  • call-manage-issue-header.ymlcontents: read
  • call-pull-request-target.ymlcontents: read
  • call-update-pr-spreadsheet.ymlcontents: read
  • community-contribution-labeling.ymlcontents: read
  • unassign-inactive.yamlcontents: read

Read-only CI

  • deploytest.ymlcontents: read, actions: read
  • frontendtest.ymlcontents: read, actions: read
  • pythontest.ymlcontents: read, actions: read
  • pre-commit.ymlcontents: read (pre-commit-ci/lite-action does not push commits itself; elevate at job level if autofix-and-commit-back is later enabled)

Container build / publish

  • containerbuild.ymlcontents: read, packages: write (the postgres job pushes to ghcr.io on non-PR runs; the nginx job only test-builds and can be tightened at job level)

Manual translation flows (writes via bot token)

  • i18n-download.ymlcontents: read (PR creation handled by peter-evans/create-pull-request using an LE app token)
  • i18n-upload.ymlcontents: read

Verification

  • All workflows still run end-to-end after the change (no permission errors in logs).
  • Pre-existing job-level permissions: blocks reviewed for consistency with the new top-level block.
  • No new workflows added since this audit are left without a permissions: block — consider a pre-commit / CI rule to enforce.

References

AI usage

Drafted with Claude during a CodeQL triage session. Claude ran an audit pass over the 14 affected workflow files, matched each workflow's operations to the corresponding permission scopes, and proposed the minimal blocks in the AC checklist. Each entry should be verified against the file itself before applying.

Metadata

Metadata

Assignees

Type

No fields configured for Task.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions