Javacodegeeks iconJavacodegeeksSep 23, 2026 ~6 min source read

Hardening GitHub Actions with Least Privilege

A practical guide to reduce CI/CD risk by granting the minimum GITHUB_TOKEN permissions per job, favoring job-level scoping, and separating read-only CI from privileged publish/deploy steps.

Hardening GitHub Actions with Least Privilege

Share this story

Send the public story page.

Useful takeaways from this story.

Start workflows with no permissions and explicitly add only the GITHUB_TOKEN scopes each job needs.

Prefer job-level permissions over workflow-wide write access to limit what a compromised action or script can do.

# Why least privilege matters for GitHub Actions GitHub Actions supplies a temporary GITHUB_TOKEN to workflow jobs. That token can modify repository contents, create releases, publish packages, update pull requests, create deployments, and request an id-token for federated auth. If a job receives more privileges than it needs, a compromised action, dependency, or injected command can use those privileges to broaden an attacker's access. Minimizing token scope reduces that blast radius.

# Core hardening principles

  • Grant permissions explicitly inside the workflow or per job, not rely on repository/organization defaults.
  • Prefer job-level permissions so only the jobs that need write rights get them.

# How GITHUB_TOKEN permissions behave GitHub creates a temporary GITHUB_TOKEN for each job and ties its capabilities to the permissions declared for the job or workflow. Common permission scopes include contents (read/write), pull-requests (write), issues (write), packages (write), deployments (write), security-events (write), attestations (write), and id-token (write). The token expires when the job completes.

# Practical workflow pattern

  1. For each job, declare only the permissions required. For example, a build-and-test job typically needs contents: read only. A separate deploy job can be given contents: write and deployments: write.
  2. Keep steps that execute third-party actions or run uncontrolled scripts inside read-only jobs if possible. Give elevated permissions only to jobs that run trusted, audited code that must publish or change repository state.

# Example roles for jobs

  • Build/test: contents: read. Runs tests, compiles, and produces artifacts without write access.
  • Publish package: packages: write and contents: write (if pushing tags/releases). Run in a separate job that only executes after successful tests.
  • Create deployments: deployments: write. Isolate deployment credentials and actions to a dedicated job with minimal additional scopes.
  • OIDC usage: id-token: write only where federated authentication to cloud providers is required.

# Why explicit job-level permissions help reviews and portability Relying on implicit permission defaults can hide the effective scope of GITHUB_TOKEN, because defaults may vary by repo, org, or enterprise settings. Explicit permissions make the intended security boundary visible in the workflow file, which helps reviewers spot overly broad access and prevents accidental privilege expansion when copying workflows between repositories.

# Small checklist to harden a workflow

  • Audit each job and remove any permission that is not strictly required.
  • Move any write or secret-using steps into separate, minimal-permission jobs that run only after appropriate gates (tests, approvals).
  • Avoid blanket permissions: write-all or broad workflow-level write unless there is an unavoidable single reason and compensating controls.

# Bottom line Treat GITHUB_TOKEN as a scarce capability. Design CI/CD pipelines so the majority of jobs run with read-only access, and grant write or other sensitive scopes only where necessary and isolated. This reduces the potential impact of compromised actions, dependencies, or injected commands and makes workflows easier to review and re-use safely.

More context around this story.

Loading more related stories...

Keep reading in the app

Open the app view to save this story, compare related coverage, and continue from the same source.

Open in app