Federated credentials reduce the risks of token exposure and leaked secrets, but they can leave dangling trust relationships too. In my previous post, I covered subject recycling in Microsoft Entra ID: how to find GitHub Actions and GitLab trusts that rely on mutable subjects and how to switch to immutable identifiers.
This time, I’m looking at AWS IAM role trust policies. The question is the same: does the trust rely on an immutable identifier or a name that could be recycled?
As a quick recap, here’s how mutable and immutable subjects differ:
GitLab example:
-"sub": "project_path:nicolonsky/ITDR:ref_type:branch:ref:main"
+"sub": "project_id:715070358:ref_type:branch:ref:main"
GitHub example:
-"sub": "repo:nicolonsky/ITDR:ref:refs/heads/main"
+"sub": "repo:nicolonsky@32899754/ITDR@715070358:ref:refs/heads/main"
AWS IAM role trust policies#
If you’re coming from the Microsoft world, it’s worth distinguishing three core AWS IAM concepts:
- Permissions policy: Defines which actions are allowed or denied on which resources. For example,
s3:ListBucketallows listing objects in a bucket. The permissions are comparable to those in an Azure RBAC role. 1 2 - Role trust policy: Defines who can assume a role and under which conditions. In the Microsoft world, principals always refer to Entra ID objects, such as users, groups and service principals (instances of app registrations). AWS can delegate trust directly to an external identity provider without local objects. 3
- IAM role: Combines a trust policy with permissions policies. 4
Example configuration#
Before using GitHub Actions or GitLab as a federated principal, add the IdP as an OIDC provider in IAM:

Then configure the role’s permissions and reference the provider in its trust policy.
StringLike with a wildcard that allows any GitHub Actions workflow under the specified GitHub handle.
AWS CLI#
We can scan role trust policies in bulk with the AWS CLI and jq. The scan flags subject conditions without the expected immutable identifiers.

aws iam list-roles --output json --no-cli-pager |
jq -r '
["RoleName", "RoleArn", "Operator", "Claim", "Subject"],
(
.Roles[] | . as $role
| .AssumeRolePolicyDocument.Statement
| if type == "array" then .[] else . end
| select(.Effect == "Allow")
| (.Condition // {} | to_entries[]) as $condition
| $condition.value | to_entries[]
| select(.key | endswith(":sub"))
| .key as $claim
| .value | if type == "array" then .[] else . end
| select(type == "string")
| select(
if $claim == "token.actions.githubusercontent.com:sub" then
(test("repo:.+@\\d+/.+@\\d+:|repository_id"; "i") | not)
elif test("project_id|project_path|ref_type"; "i") then
(test("project_id"; "i") | not)
else false end
)
| [$role.RoleName, $role.Arn, $condition.key, $claim, .]
)
| @tsv
'For a broader review, list roles that allow sts:AssumeRoleWithWebIdentity, including through wildcard actions. This query excludes Cognito-only trust statements (cognito-identity.amazonaws.com) and returns the full trust policies for manual review:

aws iam list-roles --output json --no-cli-pager |
jq '
[
.Roles[]
| select(any(
.AssumeRolePolicyDocument.Statement
| if type == "array" then .[] else . end;
.Effect == "Allow" and any(
(.Principal | if type == "object" then .Federated // [] else . // [] end) | if type == "array" then .[] else . end;
. != "cognito-identity.amazonaws.com"
) and any(
.Action // [] | if type == "array" then .[] else . end;
. as $action
| "sts:AssumeRoleWithWebIdentity"
| test(
"^" + ($action | gsub("\\*"; ".*") | gsub("\\?"; ".")) + "$";
"i"
)
)
))
| {RoleName, Arn, AssumeRolePolicyDocument}
]
'Recap#
OIDC and federation are vendor-agnostic, and so are the risks of weak or misconfigured workloads. Searching for mutable subjects can help identify privilege escalation paths during security assessments or red-team engagements.
