Every GitHub Actions job receives a GITHUB_TOKEN, and any step in that job, including third-party actions, can use whatever that token is allowed to do. This guide covers one task: moving an organisation from broad token permissions to read-only defaults with explicit per-job scopes, without breaking builds, and then auditing that it stays that way. It is written for platform engineers and organisation owners on GitHub.com who already know the basics of Actions. For the wider credential picture, including OIDC, GitHub Apps and runners, see the GitHub and GitHub Actions Security Guide; this page goes deeper on the token alone.
This is an independent guide from NHI Mgmt Group, based on GitHub's public documentation as of November 2026. It is not sponsored or reviewed by GitHub. Product features change, so confirm settings against GitHub's current documentation.
Key takeaways
- Two layers set the token's power. The default at enterprise, organisation or repository level, then the workflow's
permissionskey, workflow level first and job level after. - Listing one scope sets the rest to none. GitHub's workflow syntax states that once any permission is specified, all unspecified ones become none, which is what makes per-job blocks safe.
- Set the default to read, then grant write per job. The restricted default gives read access to contents and packages only; jobs that publish, comment or deploy ask for exactly what they need.
- Fork and trigger rules matter as much as scopes.
pull_request_targetreceives a read and write token even from a public fork, so treat those workflows as privileged. - Audit by API and by file. Default settings are readable through the REST API, and workflow files can be scanned for missing or broad
permissionsblocks on a schedule.
When you need this
- Organisation default is still read and write: older organisations and repositories may have been created when the permissive default was common. Every workflow without a
permissionskey inherits it. - Third-party actions in use: GitHub notes that an action can read the token through the
github.tokencontext even when the workflow does not pass it explicitly. The token's scope is the blast radius of any action you run. - Audit or certification finding: a least privilege finding on CI credentials, or a low OpenSSF Scorecard Token-Permissions result, which the Scorecard project rates as a high-risk check.
- After a workflow incident: tightening scopes is a standard hardening step once a compromised action or injection has been contained. See the CI/CD Pipeline Compromise Playbook.
Before you start
- Access: organisation owner for the organisation default; repository admin for repository settings. Reading the defaults by API needs the Administration permission (read) on a fine-grained token or GitHub App, or
admin:org(organisation) orrepo(repository) on a classic token. - Inventory workflows: list every
.github/workflows/*.ymlfile across repositories, and note which have a top-levelpermissionskey, which set it per job, and which have none. - Know what writes: for each workflow without a
permissionskey, work out what it changes in GitHub: pushes, tags, releases, comments, labels, packages, Pages, deployments, code scanning uploads. That list becomes its job-level grants. - Pick a pilot: a handful of busy repositories with different workflow styles, so failures show up early.
Step-by-step
1. Add explicit permissions to workflows first
Start in the files, because a workflow with its own permissions key behaves the same whatever the default is. That lets you flip the default later with little breakage.
- Top of every workflow: add
permissions: {}(no access) orpermissions: contents: read. Scorecard's remediation advice isread-allorcontents: readat the top level;contents: readis the narrower of the two. - Per job: add
jobs.<job_id>.permissionswith only the scopes that job uses. Job-level settings apply after the workflow-level ones, so a release job can havecontents: writewhile the test job stays read-only. - Remember the reset: because unlisted scopes become none, a job that sets
issues: writelosescontents: readunless you list it too. Jobs that check out code needcontents: readin their own block. - Avoid
write-all: it grants write on every scope that accepts it. If a job seems to need it, split the job.
| Job does this | Grant | Notes |
|---|---|---|
| Checks out code, builds and tests | contents: read | Enough for most jobs |
| Pushes commits, creates tags or releases | contents: write | Keep in a separate release job |
| Comments on or labels pull requests | pull-requests: write | Add contents: read if it also checks out |
| Comments on or labels issues | issues: write | |
| Publishes to GitHub Packages or the container registry | packages: write | Restricted default already gives packages read |
| Uploads code scanning (SARIF) results | security-events: write | |
| Publishes GitHub Pages | pages: write and id-token: write | GitHub's Pages docs give both as the minimum for the deploy job |
| Requests an OIDC token for cloud access | id-token: write | Accepts only write or none; see the GitHub Actions OIDC Federation Hardening Guide |
| Creates build provenance attestations | attestations: write | GitHub's attestation docs pair it with id-token: write and contents: read, plus packages: write for container images |
| Manages runs, caches or artifacts by API | actions: write | Rarely needed; review carefully |
The full list of scopes in GitHub's workflow syntax reference as of November 2026 is: actions, artifact-metadata, attestations, checks, code-quality, contents, copilot-requests, deployments, discussions, id-token, issues, packages, pages, pull-requests, security-events, statuses and vulnerability-alerts. Some accept only certain values: vulnerability-alerts takes read or none, while id-token and copilot-requests take write or none.
2. Set the restricted default
- Organisation: in the organisation's Actions settings, under Workflow permissions, select the read-only option for contents and packages. GitHub states that when an organisation selects the more restrictive default, its repositories inherit it and the permissive option is disabled for them.
- Enterprise: where you run GitHub Enterprise Cloud, set the same default at enterprise level so new organisations start restricted. A restricted default at a higher level applies to everything beneath it.
- Repository (personal accounts or exceptions): Settings > Actions > General > Workflow permissions. GitHub states new repositories in a personal account start with read access for contents and packages only.
- Create and approve pull requests: in the same section, leave "Allow GitHub Actions to create and approve pull requests" off unless a named automation needs it. GitHub's REST reference flags enabling the related
can_approve_pull_request_reviewsfield as a security risk, because it lets a workflow approve its own changes. - By API:
PUT /orgs/{org}/actions/permissions/workfloworPUT /repos/{owner}/{repo}/actions/permissions/workflowwithdefault_workflow_permissionsset toreadandcan_approve_pull_request_reviewsset tofalse. A repository call returns 409 when the organisation blocks the change.
3. Lock down fork and privileged triggers
- Fork pull requests: for pull requests from forks, GitHub downgrades write permissions to read-only unless "Send write tokens to workflows from pull requests" is selected. For private repositories, leave that setting and "Send secrets to workflows from pull requests" off, and keep "Require approval for fork pull request workflows" on.
pull_request_target: receives a read and write repository token even from a public fork. Give these workflows explicit minimalpermissionsblocks, never check out and run the pull request's code in them, and list them for review.- Dependabot runs: GitHub treats Dependabot-triggered runs like fork runs: read-only token and no secrets. Workflows that need to act on Dependabot pull requests should be designed for that rather than widened.
- Do not swap in a broad personal access token: events triggered by the GITHUB_TOKEN do not start new workflow runs, apart from
workflow_dispatchandrepository_dispatch, and pull requests it opens wait for approval. GitHub suggests a GitHub App installation token or a personal access token if runs must chain; prefer a narrowly installed GitHub App. See the GitHub Personal Access Tokens Security Guide.
Verify it worked
- Defaults by API:
GET /orgs/{org}/actions/permissions/workflowreturnsdefault_workflow_permissions: readandcan_approve_pull_request_reviews: false. Run the repository variant across all repositories to find any set differently. - Workflow files: scan every workflow for a top-level
permissionskey and forwrite-all. Scorecard's Token-Permissions check does this per repository and warns on sensitive job-level write scopes such ascontents,packagesandactions. Scorecard notes it cannot see the repository default setting, so pair it with the API check. - Negative test: in a pilot repository, add a step that tries a write the job was not granted (for example, labelling an issue from a read-only job) and confirm it fails.
- Change watch: alert on changes to Actions settings in the audit log. See the GitHub Audit Log Detection Guide.
| Metric | Target (our suggestion) | Source |
|---|---|---|
| Organisations and repositories with read default | 100% | REST API |
Workflows with a top-level permissions key | 100% of active workflows | File scan or Scorecard |
Workflows using write-all | Zero | File scan |
pull_request_target workflows reviewed in the last quarter | All | Review log |
| Repositories allowing Actions to approve pull requests | Zero, or named exceptions | REST API |
Rollback and common errors
The default is one setting and can be switched back at the organisation or repository level in seconds, so rollback is easy. The better rollback is narrower: when a workflow fails, add the missing scope to that job instead of reopening the default for everyone.
| Symptom | Likely cause | Fix |
|---|---|---|
| Release job fails to push a tag or create a release | Job has contents: read | Grant contents: write to that job only |
Checkout fails after adding issues: write | Unlisted contents became none | Add contents: read to the same block |
| Bot comment on pull request fails | Missing pull-requests: write | Grant it to the commenting job |
| Cloud login step fails | Missing id-token: write | Grant it to the deploying job only |
| Repository setting cannot be changed (409 by API) | Organisation policy controls it | Change at organisation level or leave as is |
| Fork pull request workflow cannot write | Expected fork downgrade | Split into an unprivileged check and a trusted follow-up workflow |
Hardening afterwards
- Pin actions: least privilege limits what a compromised action can do; pinning to a full commit SHA limits which code runs. See the GitHub Organisation Security Checklist.
- Move long-lived secrets out: use OIDC federation for cloud access rather than stored keys. See How to set up workload identity federation for GitHub Actions to AWS.
- Guard against injection: scoped tokens reduce the damage of script injection and poisoned pipeline execution but do not prevent them. See the OWASP CI/CD Top 10 CICD-SEC-4 Poisoned Pipeline Execution Guide.
- Review on change: add a CODEOWNERS entry for
.github/workflows/so any newpermissionsgrant gets a second reviewer.
Where this sits in common frameworks (our mapping):
| Framework | Reference | How this task supports it |
|---|---|---|
| NIST SP 800-53 Rev 5 | AC-6 Least Privilege | Per-job scopes and read-only defaults |
| NIST SP 800-53 Rev 5 | CM-2 Baseline Configuration | Organisation-wide default and workflow file standard |
| NIST CSF 2.0 | PR.AA-05 | Access permissions defined, enforced and reviewed with least privilege |
| NIST CSF 2.0 | PR.PS-06 | Secure software development practices in the pipeline |
Practitioner checklist
- Inventory workflows and record which lack a
permissionskey. - Add a top-level
permissions: {}orcontents: readto every workflow. - Grant write scopes per job, listing
contents: readwhere a job also checks out code. - Set the restricted default at enterprise and organisation level.
- Turn off "Allow GitHub Actions to create and approve pull requests" except for named automations.
- Keep fork write tokens and fork secrets off; require approval for fork workflows.
- Review every
pull_request_targetworkflow and give it minimal explicit scopes. - Check defaults by REST API and workflow files by scan or Scorecard on a schedule.
- Alert on Actions settings changes in the audit log.
Standards and references
- GitHub Docs: GITHUB_TOKEN
- GitHub Docs: Workflow syntax for GitHub Actions, permissions
- GitHub Docs: GITHUB_TOKEN reference
- GitHub Docs: Managing GitHub Actions settings for a repository
- GitHub Docs: REST API endpoints for GitHub Actions permissions
- GitHub Docs: Using custom workflows with GitHub Pages
- GitHub Docs: Using artifact attestations
- OpenSSF Scorecard: Checks, Token-Permissions
- NIST: SP 800-53 Rev 5, Security and Privacy Controls
Related NHI Mgmt Group resources: GitHub and GitHub Actions Security Guide · GitHub Organisation Security Checklist · CI/CD Pipeline Identity Security Guide · OpenSSF Scorecard Guide