
❌ 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
Read-only CI
Container build / publish
Manual translation flows (writes via bot token)
Verification
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.
❌ 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'sGITHUB_TOKENonly 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 defaultGITHUB_TOKENpermission set, which is typicallyread-all(orwrite-allon 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 outstandingactions/missing-workflow-permissionsalerts 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_KEYviaactions/create-github-app-token, orpeter-evans/create-pull-requestwith a generated app token) for any write operation, theGITHUB_TOKENitself can stay locked down — only what the workflow's own steps need.A few cases worth calling out:
call-*shims pluscommunity-contribution-labeling.ymlandunassign-inactive.yaml): when a job usesuses: 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, socontents: readis sufficient.pull_request_targetworkflows (call-contributor-pr-reply.yml,call-pull-request-target.yml,call-update-pr-spreadsheet.yml): privileged trigger. Bot-token-based work is preferred over expandingGITHUB_TOKENpermissions.containerbuild.yml: pushes images toghcr.ioon non-PR runs (logs in via${{ secrets.GITHUB_TOKEN }}). Requirespackages: writeat the top level so the push step has the scope it needs.Out of Scope
peter-evans/create-pull-request,fkirc/skip-duplicate-actions).learningequality/.githubworkflows called by thecall-*shims.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 delegatorscall-contributor-issue-comment.yml—contents: readcall-contributor-pr-reply.yml—contents: readcall-manage-issue-header.yml—contents: readcall-pull-request-target.yml—contents: readcall-update-pr-spreadsheet.yml—contents: readcommunity-contribution-labeling.yml—contents: readunassign-inactive.yaml—contents: readRead-only CI
deploytest.yml—contents: read,actions: readfrontendtest.yml—contents: read,actions: readpythontest.yml—contents: read,actions: readpre-commit.yml—contents: 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.yml—contents: 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.yml—contents: read(PR creation handled bypeter-evans/create-pull-requestusing an LE app token)i18n-upload.yml—contents: readVerification
permissions:blocks reviewed for consistency with the new top-level block.permissions:block — consider a pre-commit / CI rule to enforce.References
actions/missing-workflow-permissionsAI 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.