Skip to main content
INS // Insights

CI Role Access Review for Deploy Rights

Updated October 2026 · 8 min read

A CI role access review treats the deploy role as an identity. Nobody logs in as that role with a password. A pipeline assumes it, usually through OpenID Connect, and then the role's policies decide what a merge can change in production. The review has two halves: which workflows are allowed to assume the role, and which actions the role can perform after the assumption. Either half alone is an incomplete picture of deploy rights. A tight trust policy on a role that can pass a privileged task role is still a wide door. A narrow policy on a role whose trust accepts any branch, including pull requests, is also a wide door.

Rutagon builds this review for teams that already federate GitHub Actions, GitLab, or a similar runner into AWS and still certify access quarterly for SOC 2. Human access reviews do not list these roles unless someone puts them in the population on purpose. We ingest the role, the trust, the attached policies, and the recent assumptions from CloudTrail, and we put one row per role in the campaign. The reviewer is the service owner, not "the whole engineering team." AWS documents the OIDC provider setup in Creating OpenID Connect identity providers. The review starts after that provider exists, because that is when the role became assumable by a pipeline.

What a CI role access review has to include

The trust policy is the first column. For GitHub Actions the subject claim is the usual control, and GitHub documents the token in configuring OIDC in Amazon Web Services. A subject of repo:org/name:ref:refs/heads/main is a different grant from repo:org/name:*, and both are different from a subject that allows pull_request. We parse the StringEquals and StringLike conditions on token.actions.githubusercontent.com:sub and on the audience. A trust that only matches the issuer, with no subject condition, is recorded as open to any token that issuer will sign. That row is privileged by default.

The audience check matters as much as the subject. The standard audience is sts.amazonaws.com. A missing audience condition means a token minted for a different relying party can still satisfy the trust if the subject happens to match. We flag a trust with no audience condition even when the subject looks tight. GitHub Actions OIDC at scale is the pattern for many repositories. This review is the recurring look at those trusts after the pattern has been live for a quarter.

Environment claims are the second column, when the workflow uses a GitHub environment. The subject can include repo:org/name:environment:production. That is only a control if the environment has the protection rules you think it has. GitHub environment protection with OIDC is where those rules belong. The review row stores whether the subject is environment-scoped and whether we could read a required-reviewer rule for that environment. A subject that says environment:production with no required reviewer is a name, not a gate.

The permission half is the deploy right itself. We expand attached managed policies and inline policies to the actions that change production: updating a service, publishing a function, copying objects into the production bucket, and iam:PassRole. PassRole is the one teams skip. The pipeline role may not be allowed to read customer data, and it may still be allowed to pass a task role that can. The review row lists PassRole targets separately from the rest of the action list. A reviewer who only sees "deploy" will approve a role that can hand a broader role to a running task.

CloudTrail is the third column. AssumeRoleWithWebIdentity events for the role over the last 90 days show which subjects actually assumed it. A trust that allows three repositories, with assumptions from only one, is a candidate to narrow. A trust that allows only main, with assumptions whose subject is a pull request, means the policy you are reading is not the policy that was in force, or the subject parse is wrong. We show both the policy and the observed subjects. Disagreement between them is the item, not a footnote.

A row the reviewer can decide

The campaign item is one role. The fields are the ones a service owner can judge without opening the IAM console.

role_arn: arn:aws:iam::111122223333:role/deploy-checkout
trust_subs:
  - repo:acme/checkout:ref:refs/heads/main
trust_audience: sts.amazonaws.com
allows_pull_request: false
pass_role_targets:
  - arn:aws:iam::111122223333:role/checkout-task
actions:
  - ecs:UpdateService
  - ecs:RegisterTaskDefinition
last_assumption_subjects:
  - repo:acme/checkout:ref:refs/heads/main
reviewer: checkout-service-owner

The example account id is the documentation sample, not a client account. The shape is what we load. allows_pull_request is derived, so the reviewer does not have to remember which subject strings include fork pull requests. pass_role_targets is derived from the identity policies. If the policy uses a wildcard on PassRole, the target list is the wildcard and the row is marked broad.

The reviewer has the same two decisions as any other entitlement: maintain, with a reason, or revoke. Revoke on a CI role is not a user-disable call. It is a change to the trust, a detach of a policy, or a narrower PassRole resource. We open that change in the Terraform that owns the role. Terraform for AWS IAM and CI OIDC is how the role was created, and it is how the review decision sticks. A console edit is gone on the next apply. The evidence row stores the pull request that carried the tighten, not only the click in the review tool.

Maintain is also specific. "Still needed for deploy" is a reason code. It is accepted when the subject is limited to the protected branch or the protected environment, and when PassRole targets are explicit role ARNs. Maintain is rejected, and the item stays open, when the subject is a wildcard or the PassRole resource is *. The owner can still decide to keep it, and then the reason code is exception with an expiry. The exception shows up in the next campaign automatically. An exception with no expiry is the new standing grant.

We group roles by the production boundary they can touch. A role that can update only a staging service is in the staging campaign. A role that can update production, or pass a production task role, is in the privileged campaign with a shorter due date. Mixing them in one queue is how the production role gets the same glance as the docs-site publisher. Privileged access review automation is the surrounding campaign for human admin. CI deploy roles belong in that privileged population when their actions reach production. Leaving them in a separate spreadsheet is how they miss the quarter.

What we record after the decision

The evidence for the period is the row plus the decision. For a maintain, we store the decider, the time, the reason code, and a hash of the trust policy and the identity policies that were reviewed. If the trust changes the next day, the hash no longer matches and the item is stale. We do not treat last quarter's approval as approval of a trust someone edited in the console on Monday.

For a revoke, we store the decider, the time, the Terraform pull request, and the CloudTrail observation after the change: a later assumption either fails or comes from the narrowed subject. The campaign item closes when that observation matches the new trust. A merged pull request with no later assumption, and no failed assumption, stays open through the next business day so we can see the pipeline run. If the pipeline did not run, we record awaiting_next_deploy and the service owner names the date of the next production deploy. The item does not close on the merge alone.

Shared roles are called out. One deploy role used by eight repositories means eight subjects, eight service owners, and one set of policies. We fan the campaign out to one item per subject, and a policy change requires every owner who is still on the role. The better end state is a role per service, which is a build we do when the fan-out is how reviews stall. The review makes that stall visible. The split is a follow-on change with its own pull request.

Rutagon's deliverable is the ingest of trust and policy, the derived flags, the campaign row, and the Terraform change that implements revoke. Human user reviews continue on their own population. This one is how deploy rights get a decider.

Frequently Asked Questions

Who is the reviewer for a CI role?

The service owner of the system the role can change. A shared platform role is fanned out by subject so each repository owner decides their own assumption. A single shared approval from "devops" with no service owner is the row we flag before the campaign starts.

Is the trust policy enough to approve deploy rights?

The trust decides who can assume the role. The identity policy decides what the role can do, including iam:PassRole to a task role. The review item shows both, plus the subjects that actually assumed the role in CloudTrail. An approval stores a hash of both policies.

What does revoke mean if there is no user to disable?

Revoke is a trust change, a policy detach, or a narrower PassRole target, made in the Terraform that owns the role. The item closes after a later assumption matches the new trust, or after a failed assumption shows the old path is gone. A console click is not the record we keep.

How do pull request workflows show up?

They show up as subject strings that include pull_request, or as a wildcard subject that would accept them. The row has a derived flag so the reviewer does not have to memorize the claim format. A maintain decision on that flag requires an exception with an end date.

How does this relate to human admin reviews?

Human admin seats stay in the privileged user campaign. A CI role joins that privileged population when it can change production or pass a production role. Staging-only roles stay in a separate campaign so the production decision is not buried in the list.

Ask for an access-review diagnostic of the roles your pipelines assume and the production actions those roles still have. Contact Rutagon at rutagon.com/contact, 907-841-8407, or contact@rutagon.com.