The GITHUB_TOKEN is generated automatically for every workflow run. It lets
you talk to the GitHub API: clone the repository, open issues, publish
packages. Badly configured, it becomes a major attack vector.
What you will learn
- Apply least privilege to the
GITHUB_TOKEN - Declare permissions explicitly in every workflow
- Choose between workflow-level and job-level permissions
- Know each available permission and the risk attached to it
- Check compliance with Scorecard's Token-Permissions check
The principle of least privilege
The principle of least privilege means granting only the permissions that are strictly necessary. If a workflow is compromised, the damage stays limited to what that workflow was allowed to do.
Default permissions
The default permissions of the GITHUB_TOKEN depend on the age of the
repository and on its configuration. It is a classic trap: an old repository
still grants broad write rights by default.
| Repository type | Default permissions |
|---|---|
| Created after February 2023 | contents: read only |
| Created before February 2023 | Read AND write on everything |
Check your settings
Under Settings > Actions > General > Workflow permissions, select "Read repository contents and packages permissions" to force minimal default permissions.
Declaring permissions explicitly
Even with restrictive repository settings, always declare the permissions in your workflows: the repository configuration can change, whereas the workflow stays explicit and auditable.
name: Build and Test
permissions: contents: read # Read the code only
on: push: branches: [main]
jobs: build: runs-on: ubuntu-24.04 steps: - name: Check out the code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install and test run: npm ci && npm testWorkflow-level versus job-level permissions
GitHub Actions lets you declare permissions at two levels. The choice between them decides the blast radius if a single job is compromised.
Workflow-level
Permissions declared at the workflow level apply to every job, including those that do not need them.
# ⚠️ Every job holds packages:writepermissions: contents: read packages: write
jobs: test: runs-on: ubuntu-24.04 steps: - run: npm test # Does not need packages:write
publish: runs-on: ubuntu-24.04 steps: - run: npm publishJob-level (recommended)
Keep the workflow level to the strict minimum and declare write permissions
on each job that genuinely needs them.
permissions: contents: read # Minimal for the workflow
jobs: test: runs-on: ubuntu-24.04 permissions: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - run: npm test
publish: needs: test runs-on: ubuntu-24.04 permissions: contents: read packages: write # For this job only steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - run: npm publishThe benefit: if the test job is compromised, the attacker cannot publish
a package.
Available permissions
Here are the permissions you will grant most often, ranked by risk. The
rule stays the same: enable write only on the job that strictly needs it.
| Permission | Use | Risk if abused |
|---|---|---|
contents: read | Clone the repository | Low |
contents: write | Push, create releases | Code injection |
packages: write | Publish to GitHub Packages | Malware distribution |
actions: write | Modify the workflows | Persistence |
id-token: write | OIDC for cloud providers | Infrastructure access |
pull-requests: write | Comment on pull requests | Spam, phishing |
issues: write | Create or edit issues | Spam |
security-events: write | Upload SARIF | Hiding alerts |
Never use this
permissions: write-allA workflow holding every permission can modify its own code, create malicious releases and persist inside the repository.
Scorecard Token-Permissions
OpenSSF Scorecard checks that
your workflows respect least privilege. The Token-Permissions check
requires write permissions to sit at the job level.
# Check your workflowsscorecard --local . --checks Token-Permissions --show-detailsScore 0/10: write permissions at the workflow level.
# ❌ Scorecard: 0/10permissions: contents: read packages: write # At the workflow levelScore 10/10: write permissions at the job level only.
# ✅ Scorecard: 10/10permissions: contents: read
jobs: publish: permissions: packages: write # At the job levelA practical case: a complete CI/CD pipeline
This full pipeline, test, build, publish, applies least privilege job by job. It is a directly reusable model: each job receives only what it needs.
name: CI/CD Pipeline
permissions: contents: read
on: push: branches: [main] pull_request:
jobs: test: runs-on: ubuntu-24.04 permissions: contents: read steps: - name: Check out the code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Install and test run: npm ci && npm test
build: needs: test runs-on: ubuntu-24.04 permissions: contents: read steps: - name: Check out the code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Build the project run: npm run build - name: Upload the artefact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: dist path: dist/
publish: if: github.ref == 'refs/heads/main' needs: build runs-on: ubuntu-24.04 permissions: contents: read packages: write id-token: write # For OIDC steps: - name: Check out the code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Download the artefact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: dist path: dist/ - name: Publish the package run: npm publishKey points
- The
GITHUB_TOKENis created for every run; an old repository grants it broad write rights by default, which is worth fixing. - Declare
permissions:explicitly in every workflow, even when the repository settings are already restrictive. - Keep the workflow level minimal (
contents: read) and grantwriteonly to the job that genuinely needs it. permissions: write-allmust be banned: a compromised workflow could modify its own code and persist.- Scorecard's Token-Permissions check requires
writepermissions at the job level; that is the compliance signal.
For the exhaustive list of permissions, see the official documentation on the GITHUB_TOKEN.
Next steps
- OIDC: authenticating without a secret: the
id-token: writepermission in action, replacing static cloud credentials with a token that lives a few minutes. - Cloud OIDC (AWS, Azure, GCP): the provider-side configuration that grants those rights to the right repository and the right branch.
- Security checklist: the recap that puts least privilege back among the other controls to verify before publishing.