Safeguard
Software Supply Chain•

How do you limit GitHub Actions token permissions to least privilege?

Set the default GITHUB_TOKEN to read only, grant write per job, pin actions to commit SHAs and use OIDC for cloud access. Here is the order to do it in.

Safeguard Security Team
5 min read

To limit GitHub Actions token permissions, set the default GITHUB_TOKEN to read access for repository contents, then raise permissions only on the specific job that needs them using the permissions key. Replace long-lived cloud secrets with OpenID Connect and pin third-party actions to full commit SHAs.

Why does the token matter so much?

Every workflow run gets a GITHUB_TOKEN. GitHub's documentation states that an action can reach it through the github.token context even when your workflow never passes it in. So a compromised or careless third-party step in a job inherits whatever that token can do.

The same security guide notes that anyone with write access to a repository can read all secrets configured in it. Together, those two facts set the rule: assume any step in a job can use everything the job holds, and give the job less.

What does a least privilege workflow look like?

Start at the top of the file with a restrictive default, then loosen one job at a time.

name: release
on:
  push:
    tags: ["v*"]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<full-length-commit-sha>
      - run: make test

  publish-notes:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@<full-length-commit-sha>
      - run: ./scripts/create-release.sh

Here the test job, which runs the most third-party code, only ever holds a read token. Only the small notes job can write. If the test job is compromised, it cannot push a tag or edit a release.

Note that when you set permissions at all, any scope you do not list is set to none. That is why listing exactly what a job needs works as an allowlist.

What about the repository and organization defaults?

GitHub's guide recommends making read access to repository contents the default for the GITHUB_TOKEN, and increasing it per job. You can set that default in repository or organization settings, which protects workflows nobody remembered to annotate. Treat the workflow file as the source of truth anyway, because settings can be changed by an admin without a code review.

Should you pin actions to a SHA?

GitHub's guide says to pin an action to a full-length commit SHA, because an attacker would need to produce a SHA-1 collision for a valid Git object to slip a backdoor in. Check that the SHA belongs to the action's own repository and not a fork. A tag is only acceptable if you trust the author, since tags can be moved or deleted.

Add a Dependabot version update entry for the github-actions ecosystem so the pinned SHAs keep moving forward instead of rotting.

How do you stop storing cloud keys?

If a job deploys to a cloud provider that supports OpenID Connect, let the workflow authenticate directly with a short-lived token instead of a stored access key. GitHub's guide points to this as the way to stop keeping credentials as long-lived secrets. The cloud side then limits which repository, branch or environment may assume the role.

Which triggers need extra care?

pull_request_target and workflow_run, when combined with checking out an untrusted pull request, are called out in GitHub's guide as exposing the repository to compromise, because those workflows are privileged and can reach secrets. If you use them, never run code from the pull request in the privileged context.

Also handle untrusted input safely. Do not paste values like a pull request title straight into a shell script. Pass them through an intermediate environment variable, or hand them to an action as an argument.

What to do

  1. Add a top-level permissions: contents: read block to every workflow.
  2. Grant write scopes only on the one job that needs each of them.
  3. Pin every third-party action to a full-length commit SHA and let Dependabot update them.
  4. Replace stored cloud keys with OIDC and restrict the trust policy to specific repositories and environments.
  5. Audit pull_request_target and workflow_run workflows for untrusted checkouts.
  6. Put .github/workflows under CODEOWNERS so changes need a named reviewer.

Safeguard can help with step 3 and 5 at scale: its policies and gates can flag unpinned actions and risky workflow patterns in pull requests, and its dependency scanning covers what the workflow installs.

Quick check

If I set permissions on one job, do the others keep the default?

A job-level permissions block applies to that job. Jobs without one use the workflow-level block, or the repository default if the workflow sets none. Setting a top-level block avoids guessing.

Is a tag like v4 safe enough for an action?

Only if you trust the action's author, per GitHub's guidance. Tags can be moved or deleted. A full-length commit SHA cannot be silently repointed.

Sources

Last reviewed: 18 October 2026.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.