Add top-level permissions: blocks to GitHub Actions workflows (least-privilege GITHUB_TOKEN)
Most of our workflows don't declare a top-level permissions: block, so their GITHUB_TOKEN inherits whatever the repository-level default happens to be. That default can be widened by an admin without any diff or PR — the workflow itself gives no indication of what scope its token actually has at runtime.
Where we are today
24 of 39 workflows have no top-level permissions: in upstream/master:
.github/workflows/actionlint.yml.github/workflows/check-performance-regression.yml.github/workflows/check-pr-description.yml.github/workflows/checker-framework.yml.github/workflows/error-prone.yml.github/workflows/google-java-format.yml.github/workflows/no-error-workflow.yml.github/workflows/no-exception-workflow.yml.github/workflows/no-old-refs.yml.github/workflows/pitest.yml.github/workflows/qodana.yml.github/workflows/release-copy-github-io-to-sourceforge.yml.github/workflows/release-maven-perform.yml.github/workflows/release-publish-releasenotes-twitter.yml.github/workflows/release-update-github-io.yml.github/workflows/release-update-github-page.yml.github/workflows/release-update-xdoc-with-releasenotes.yml.github/workflows/release.yml.github/workflows/releasenotes-gen.yml.github/workflows/run-checkstyle-ast-print.yml.github/workflows/run-checkstyle.yml.github/workflows/run-link-check.yml.github/workflows/shellcheck.yml.github/workflows/verify-no-exception-configs.yml
Why it matters
Two concrete failure modes:
Blast radius on a compromised action or step. Any code that runs inside a workflow (a third-party action, a shell step, an inline script) can use
GITHUB_TOKENat whatever scope the workflow was granted. If the token hascontents: writebecause nothing declares otherwise, a compromised step can push commits, delete branches, or create releases. If the workflow declarespermissions: { contents: read }at the root, the same compromised step can only read.Silent widening. Settings → Actions → General → "Workflow permissions" controls the repo-level default. Flipping it from read-only to write is a single-click admin action with no diff, no PR, no audit trail visible to reviewers. Explicit
permissions:at the workflow level makes the intended scope readable in the workflow file itself.
The OSSF Scorecard Token-Permissions check grades this specific pattern. Our current score is 0/10 on that dimension per securityscorecards.dev.
Proposal
Set a restrictive permissions: block at each workflow root, then grant more per-job only where needed. For workflows that just compile and test:
permissions:
contents: readFor workflows that also post PR comments or labels, keep the root restrictive and widen only on the specific job:
permissions:
contents: read
jobs:
analyze:
# inherits contents: read
...
comment:
permissions:
pull-requests: write
needs: analyze
...For release workflows, the tag-pushing / release-creating job needs contents: write, but only that job — not the whole workflow.
Rollout
Suggest three PRs, sized for reviewability:
- Read-only workflows — mechanical
contents: readat the root. Coversactionlint.yml,check-performance-regression.yml,checker-framework.yml,error-prone.yml,google-java-format.yml,no-error-workflow.yml,no-exception-workflow.yml,no-old-refs.yml,pitest.yml,qodana.yml,shellcheck.yml,verify-no-exception-configs.yml,run-link-check.yml. Zero behaviour change. - PR-interaction workflows — root
contents: read, per-jobpull-requests: writewhere the job comments. Coverscheck-pr-description.yml,run-checkstyle-ast-print.yml,run-checkstyle.yml,releasenotes-gen.yml. - Release workflows — the sensitive ones. Needs a maintainer's pass on which job needs which scope. Covers the seven
release-*.yml.
Docs:
- GitHub — Assigning permissions to jobs
- GitHub — Security hardening for GitHub Actions
Happy to open the first PR (read-only workflows) once direction is agreed.
Source: checkstyle/checkstyle