The default behavior of most CI/CD platforms is to generate a token with broad permissions, because broad permissions make setup easier. That convenience is exactly what makes a compromised pipeline so damaging.
The default is usually too broad
GitHub Actions' default GITHUB_TOKEN, for example, historically defaulted to read-write across many scopes. A pipeline that only needs to read repository contents and post a comment doesn't need write access to packages, deployments, and issues — but the default often grants it.
Scope tokens to the job, not the project
Every pipeline should have exactly the permissions its specific job needs, and nothing more:
- A test job needs read access to code. Nothing else.
- A deploy job needs write access to the specific deployment target. Not to your entire cloud account.
- A release job needs permission to create a release. Not permission to modify branch protection rules.
Separate tokens by environment
A token that can deploy to staging should not be able to deploy to production. This sounds obvious, but shared deploy credentials across environments are extremely common, because they were set up once and never revisited.
Short-lived over long-lived
Where your CI/CD platform and cloud provider support it, use short-lived, dynamically-issued credentials (like AWS's OIDC federation for GitHub Actions or GitLab CI) instead of long-lived static access keys stored as secrets. A short-lived token that expires in an hour is far less valuable to an attacker than a static key valid indefinitely.
Audit token usage periodically
Permissions tend to accumulate over time as pipelines are copy-pasted and extended. Periodically review what each pipeline's token can actually reach versus what it uses — the gap is usually larger than expected.
Least privilege in CI/CD isn't a one-time setup. It's a habit of scoping down every time a new pipeline is added, instead of reusing whatever token was easiest to grab.