The pipeline is a silent admin
To let terraform apply create VPCs, databases and IAM roles, the pipeline needs near-admin rights. If someone takes it over (a malicious PR running in a job with credentials, a compromised action, an access key leaked in a log), they control the account. So pipeline permissions need as much care as human ones, or more.
Splitting plan and apply
| Role | Used for | Rights |
|---|---|---|
tf-plan |
Every pull request | Read-only (ReadOnlyAccess or narrower) plus state read and locking |
tf-apply |
Merges to main, through an approved environment | Write on exactly the services the module manages |
flowchart LR PR["Pull request<br/>(even from unknown branches)"] -- "OIDC: ref=pull_request" --> P["role tf-plan<br/>read-only"] M["Merge to main"] --> E["production environment<br/>requires a reviewer"] E -- "OIDC: environment:production" --> A["role tf-apply"]
Unreviewed code only ever runs with read rights, so it cannot change infrastructure.
OIDC and a tight trust policy
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:example-org/infra:environment:production"
}
}
}
On Azure the equivalent is a federated credential on an app registration or managed identity, with the subject limited to the repo and environment.
Narrowing rights step by step
- Start with broad rights in a sandbox account and enable CloudTrail.
- Use IAM Access Analyzer to generate a policy from the APIs actually called.
- Add a permissions boundary so the pipeline role cannot create roles more powerful than itself.
- At organisation level, SCPs (AWS) or Azure Policy block what nobody may do, such as disabling CloudTrail or using unexpected regions.
Other practices
- One account or subscription and one role per environment; the dev pipeline has no path to prod.
- No apply without a reviewed plan; apply exactly the saved
tfplanfile. - Enable deletion protection on critical resources and review every
destroyline in the plan.
Traps: one admin access key shared by every pipeline; a plan role with write rights; a trust policy allowing all of repo:example-org/*; forgetting that iam:PassRole or iam:CreatePolicyVersion can escalate to admin.