A CI job that checks formatting should not be able to publish a package. A test job should not need a production cloud credential. Yet workflows often grow by copying a job that already works, carrying its token and secrets into every new step.
The immediate failure mode is easy to miss: the build stays green. The excess authority sits idle until a compromised dependency, unsafe event trigger, or mistaken command uses it. A permission budget makes that authority visible. List what the job must read, what it may write, and which trust boundary it crosses. Then grant only those capabilities to that job.
This is not a request to make CI unusably restrictive. It is a way to make the safe route the normal route, with a clear reason for each exception.
Inventory the job's real work
Start with a table, not with a token setting. For each job, record its trigger, input trust level, outputs, and side effects.
| Job | Inputs | Needed authority | Should not have |
|---|---|---|---|
| Lint and unit tests | Repository checkout and dependencies | Read source; report status through the CI platform | Package publishing, production secrets, cloud deployment |
| Build artifact | Trusted revision and build dependencies | Read source; write a short-lived artifact | Repository administration or production deployment |
| Publish package | Reviewed release revision and build artifact | Write to the intended package registry | Broad repository write or unrelated cloud access |
| Deploy | Approved artifact and environment | Deploy to one environment with bounded identity | Access to every environment from a pull request |
The table is a design aid, not a universal matrix. A test may require a temporary database or a service fixture. Name that dependency and give it a test-scoped identity. Do not give every test the same credential used to repair production because a fixture is inconvenient to maintain.
Ask what the job does during failure as well as success. Can it comment on a pull request? Cancel another run? Upload logs that may contain secrets? Modify a release tag? A permission budget includes those side effects. “It only runs tests” is not an answer if the script can also invoke a deployment CLI with inherited credentials.
Scope the platform token by job
In GitHub Actions, the GITHUB_TOKEN is created for a workflow job. The GitHub documentation recommends granting the minimum access and supports a permissions key at the workflow or job level. Put broad defaults at the workflow boundary and add only what a particular job needs.
For example, a deliberately incomplete workflow sketch might use:
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: make test
publish:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
steps:
# Download and verify the intended artifact before publishing.
- run: echo 'Replace with the repository-specific publish step'
The snippet illustrates separation, not a copy-ready release pipeline. It does not establish a trusted artifact, pin every action, or configure an approval environment. Those decisions belong to the repository's actual release path. packages: write is only useful when that job truly publishes through GitHub Packages; a different registry needs a different identity.
GitHub's workflow syntax explains that job-level permissions apply to the job's GITHUB_TOKEN. Review the effective permission set after adding a key; do not assume that a broad organization or repository setting is the only source of authority.
Count every credential, not only GITHUB_TOKEN
Reducing GITHUB_TOKEN access is necessary but not sufficient when the job also receives a cloud credential, package token, deploy key, or third-party service secret. A shell step can use whatever identity is present in its environment or credential store.
For each credential, record:
- Purpose: the exact operation it enables.
- Scope: repository, package, account, project, environment, and permitted actions.
- Availability: which jobs and events can receive it.
- Lifetime: when it is issued, expires, and can be revoked.
- Owner: the team that approves changes and investigates use.
OpenID Connect can replace a long-lived cloud secret in some deployment paths, but it is not a magic permission switch. In GitHub Actions, id-token: write lets a job request an OIDC token; the cloud provider's trust policy decides what that token may exchange for. Review both sides. A permissive cloud trust rule can undo a carefully scoped workflow file.
Treat artifacts and caches as inputs across trust boundaries too. If a less-trusted job produces a file that a privileged job later executes or deploys, the second job needs an explicit verification step. Giving the first job read-only repository access does not make its output trustworthy.
Separate untrusted review work from privileged work
Pull requests are where CI needs broad input coverage and narrow authority. A contributor can change source files, tests, scripts, dependency manifests, and sometimes workflow-adjacent data. Those inputs should be evaluated without inheriting release credentials.
GitHub's secure-use guidance warns about privileged triggers such as pull_request_target and workflow_run when they are combined with untrusted pull request content. The dangerous combination is not the event name by itself; it is running attacker-controlled code in a context that can write to the repository or access secrets.
Keep review jobs focused on build and test evidence. Route publishing or deployment through a separate event and trust decision that verifies the revision or artifact being promoted. If a privileged workflow must consume output from a review workflow, treat that output as untrusted until validated. Never assume that “the checks passed” proves the artifact came from the revision you intend to release.
This boundary also clarifies why tests and permission design are separate subjects. When Is Changed-Test Selection Safe Enough for CI? asks which tests provide enough evidence. This article asks what those tests are allowed to do while they run.
Review a hypothetical workflow change
Suppose a repository has one job called build-and-publish. It runs on pushes and pull requests, checks out code, installs dependencies, runs tests, builds a package, and uploads it to a registry. The job has a registry token and repository write permission because the publish step needed both at some point.
Before changing YAML, identify the actual contract. Tests need source read access and a way to report results. Building may produce an artifact. Publishing should occur only for a trusted release revision and should need registry authority, not general repository write unless the release process genuinely modifies repository state.
Split the job. Run tests and build on pull requests without publish credentials. Store the build output as an artifact with a clear revision identity. In the trusted release job, verify the selected revision and artifact, require any environment approval the release policy calls for, then give that job a short-lived publishing identity scoped to the target registry. Record how to revoke it.
After the split, test the negative cases as seriously as the successful release: a forked pull request cannot see the publishing identity; an ordinary branch push cannot publish; a failed verification cannot promote an artifact; and an operator can identify which job published a given version. Those are meaningful acceptance tests for the permission budget.
The related dependency-update automation guide covers when automated changes should be allowed to merge. A CI job's permission budget is a different decision: what authority its code receives while preparing and checking any change.
Keep the budget current
Permission reviews are most useful when attached to workflow changes. A new step that comments on PRs, uploads coverage, deploys previews, or signs an artifact is a reason to revisit the table. A removed step is a reason to remove an old grant.
Make the review answer three questions: What operation now requires more authority? Which job gets it? What evidence will show that unrelated jobs still cannot use it? The Build Systems & CI/CD hub connects this security decision to the larger feedback and release system.
A good CI pipeline is allowed to do useful work. Its authority should be as deliberate as its test selection, with one owner for every capability that could change the repository, publish an artifact, or touch production. For more engineering systems writing, visit Slaptijack.