Git, CI/CD & GitOpsXCI · Least Privilege CI/CDCloudIAM
Scoped IAM roles — what each role can do; the smallest set
What you'll learn
- Distinguish action-level scoping from resource-level scoping in an IAM policy
- Write a permission policy that grants only the actions a job requires
- Use condition keys to gate sensitive actions by IP, MFA, or tag
- Inspect a role effective permissions with aws iam list-attached-role-policies and the access analyzer
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A scoped IAM role grants the smallest set of actions,
on the smallest set of resources, under the narrowest
conditions. The discipline is to enumerate the API
calls the job actually makes — for terraform apply
against one S3 prefix and one DynamoDB table, that is
four actions on two ARNs — and to deny everything else
by default. The audit point is aws iam list-attached-role-policies: every role in the account,
with the managed policies attached, is a candidate for
tightening.
The three scoping layers
flowchart LR
A["Action-level scope\nec2:DescribeInstances"] --> R["Resource-level scope\narn:aws:ec2:region:account:instance/*"]
R --> C["Condition keys\naws:RequestedRegion, aws:PrincipalTag/role=ci-apply"]
An IAM policy statement has Effect, Action, and
Resource. The fourth piece, Condition, adds a gate.
Each is a layer of scoping:
- Action-level scope. The
Actionelement lists API calls.s3:GetObjectands3:PutObjectare two different actions; grantings3:*is the antipattern. - Resource-level scope. The
Resourceelement names the ARN.arn:aws:s3:::acme-tf-state/prod/*is one prefix;*is the entire account. Some actions do not support resource scoping (for example,iam:ListUsers); for those, the only scoping is action-level. - Condition keys. The
Conditionblock adds a gate:aws:RequestedRegionfor region pinning,aws:PrincipalTag/role=ci-applyfor identity tagging,aws:SourceIpfor network pinning,iam:PermissionsBoundaryfor boundaries.
The plan role versus the apply role
The structural difference between terraform plan and
terraform apply is the difference between read-only
actions and write actions. A plan role that holds write
actions is a plan role that can be turned into an
apply by a single command swap:
ROLE=ci-tf-plan
aws iam list-attached-role-policies --role-name "$ROLE"
The output names every managed policy attached to the
role. A typical first-pass plan role in a less
disciplined account attaches AdministratorAccess or
PowerUserAccess; the audit question is “why does
plan need any of those actions?”
A scoped plan role lists each action the plan command
calls. For Terraform with an S3 backend and a DynamoDB
lock table, the plan action set is s3:GetObject,
s3:ListBucket, dynamodb:DescribeTable,
dynamodb:GetItem, dynamodb:Query, and
dynamodb:Scan — on the specific state bucket and
lock table ARNs. The apply role adds s3:PutObject,
s3:DeleteObject, dynamodb:PutItem,
dynamodb:DeleteItem, and dynamodb:UpdateItem on
the same resources.
Inspecting and tightening
The audit command lists every attached policy. The next commands retrieve the policy document and the access analyzer generates a tighter version from CloudTrail:
ROLE=ci-tf-apply
aws iam list-attached-role-policies --role-name "$ROLE"
aws iam list-role-policies --role-name "$ROLE"
aws iam get-role-policy --role-name "$ROLE" --policy-name "$ROLE"
The audit answers four questions:
- Which managed policies are attached?
- Which inline policies are attached?
- What does each inline policy grant?
- What did CloudTrail record the role actually using in the last 90 days? (IAM Access Analyzer policy generation)
A role whose access analyzer output lists fewer actions than the policy grants is a role that can be tightened. A role whose output matches the policy exactly is at the minimum.
Condition keys and permissions boundaries
Condition keys add gates that action and resource scoping cannot express. The four most useful keys for CI/CD roles:
aws:RequestedRegion— pins the role to one or more regions. A deploy role foreu-west-1cannot reachus-east-1even if the trust policy allows it.aws:PrincipalTag/role— tags the session with the role name. Conditions that match the tag allow the action only when the session was assumed through the named role.iam:PermissionsBoundary— a separate managed policy that caps the maximum permission any role with the boundary can hold. The boundary is the safety net when a future policy edit widens a role.aws:ResourceTag/environment=production— allows the action only on resources taggedenvironment=production.
Production discipline
- Enumerate the actions. Run the job under IAM Access Analyzer policy generation; the output is the minimum set.
- Name the resources.
Resource: *is a smell. Restrict by condition key if the action does not support resource-level scoping. - Use
Sidto name every statement. TheSidis the audit trail in CloudTrail. - Set a permissions boundary. The boundary is the safety net for future policy edits.
- Audit quarterly. New commands expand the permission set.
Cross-course references
- Part XCI-01 (The validation versus deployment identity) covers the structural split between validation and deployment.
- Part XLIII-04 (OIDC in AWS) covers the trust policy that decides which workflows assume the role.
- Part XC-04 (Terraform state access from CI) covers the state read/write pattern.
Quiz
Knowledge check · 4 questions
Q1. What is the smallest valid permission grant for a CI role that runs `terraform plan` against an S3-backed state?
Q2. Resource-level scoping like `Resource: arn:aws:s3:::*` on a `s3:GetObject` action is an effective least-privilege grant because it limits the action to S3.
Q3. List the four audit questions an engineer should ask about a CI IAM role, and name one condition key that can tighten the role further.
Q4. Tighten the IAM role to the minimum permission the apply job actually needs.
Team T's CI holds role ci-tf-apply with AdministratorAccess. The apply job runs terraform apply against a single S3 state bucket (acme-tf-state, prefix prod/), a single DynamoDB lock table (acme-tf-lock), and provisions EC2 instances tagged environment=production in eu-west-1. The audit finds AdministratorAccess is overkill; the team wants the minimum.
Passing score: 75%. Answers are checked in this browser.