Least-Privilege Permissions for GITHUB_TOKEN
Every workflow gets a powerful token by default; most of that power is unnecessary.
GitHub Actions injects an automatic GITHUB_TOKEN into every workflow. By default it can be broad, and a compromised action or dependency runs with whatever that token can do. The fix is least privilege: start read-only and grant exactly the scopes each job needs. This lesson shows how.
What GITHUB_TOKEN is
The GITHUB_TOKEN is a short-lived token GitHub creates per workflow run to interact with your repository: checking out code, posting statuses, publishing packages. It expires when the run ends, but during the run it is a real credential, so its scope matters.
Default to read-only
Set a restrictive default at the top of the workflow, then widen only where needed. Many workflows need nothing more than contents: read.
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm testGrant scopes per job
Override permissions on the specific job that needs more, keeping the broad default narrow. Here only the publish job gets packages: write.
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- run: ./publish.shWhy it matters
- A compromised third-party action runs with your token's permissions; a read-only token cannot push or publish.
- Per-job scoping limits the blast radius to the one job that truly needs elevated access.
- Setting an org-wide read-only default for
GITHUB_TOKENmakes least privilege the safe default everywhere.
Key takeaways
- Set a
permissions: contents: readdefault and widen only where a job needs it. - Grant elevated scopes (like
packages: write) on the specific job, not workflow-wide. - A least-privilege token limits the damage a compromised action or dependency can do.