#21644·checkstyle

Add top-level permissions: blocks to GitHub Actions workflows (least-privilege GITHUB_TOKEN)

Author: AmitKumarDeoghoriaCreated Sep 19, 2026Updated Sep 19, 2026

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:

  1. 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_TOKEN at whatever scope the workflow was granted. If the token has contents: write because nothing declares otherwise, a compromised step can push commits, delete branches, or create releases. If the workflow declares permissions: { contents: read } at the root, the same compromised step can only read.

  2. 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:

yaml
permissions:
  contents: read

For workflows that also post PR comments or labels, keep the root restrictive and widen only on the specific job:

yaml
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:

  1. Read-only workflows — mechanical contents: read at the root. Covers actionlint.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.
  2. PR-interaction workflows — root contents: read, per-job pull-requests: write where the job comments. Covers check-pr-description.yml, run-checkstyle-ast-print.yml, run-checkstyle.yml, releasenotes-gen.yml.
  3. Release workflows — the sensitive ones. Needs a maintainer's pass on which job needs which scope. Covers the seven release-*.yml.

Docs:

Happy to open the first PR (read-only workflows) once direction is agreed.