Trust policies: known-good shapes and the mistakes that bite
Copyable trust policies for a service role, a cross-account role, a third-party role and GitHub Actions OIDC, then the known-bad versions with the symptom each produces.
markdownpatternv4VZNY-HXEYstage: buildtrust: officialtrust: fieldservice: iamservice: stsinterface: iactrust policyassume role policyPrincipalsts:AssumeRoleAssumeRoleWithWebIdentityExternalIdGitHub Actions OIDCaws:PrincipalArnInvalid principal in policy
Trust policies
A trust policy answers one question: who may become this role. It is the role's resource-based policy, so it has a Principal; what the role may do lives in its separate permissions policies. Name the narrowest principal that works, and add a condition whenever the principal is broader than one identity. Replace <placeholders> before use.
Known-good
1. A service role (Lambda, EC2, ECS, …)
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "<service>.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}
Service principal names are case-sensitive and vary by service: copy the exact name from that service's documentation. Several services go in one array under a single Service key. "Service": "*" is not allowed. The person creating the resource that uses this role also needs iam:PassRole on it; see guides/iam/create-service-role.md.
2. A role another account of yours may assume
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::<caller-account-id>:role/<caller-role>" },
"Action": "sts:AssumeRole"
}]
}
Naming a specific role is narrower than naming the account. "AWS": "<account-id>" and "AWS": "arn:aws:iam::<account-id>:root" are equivalent and mean the account: any identity there whose own policy allows sts:AssumeRole on this role can use it. Either way, the caller also needs that Allow in its own account.
To require MFA, add the condition the AssumeRole reference documents:
"Condition": { "Bool": { "aws:MultiFactorAuthPresent": "true" } }
The caller must then pass SerialNumber and TokenCode.
3. A role a third party assumes into your account
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::<vendor-account-id>:root" },
"Action": "sts:AssumeRole",
"Condition": { "StringEquals": { "sts:ExternalId": "<value the vendor gave you>" } }
}]
}
The external ID is what stops another customer of the same vendor from steering the vendor into your role (the cross-account confused deputy). It is 2–1,224 characters of letters, digits and +=,.@:/-.
4. GitHub Actions through OIDC, locked to one repository and branch
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::<account-id>: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:<org>/<repo>:ref:refs/heads/<branch>"
}
}
}]
}
For a GitHub environment, the subject is repo:<org>/<repo>:environment:<environment>, and GitHub recommends adding protection rules to that environment. The OIDC provider is created once per account, with URL https://token.actions.githubusercontent.com and audience sts.amazonaws.com; the configure-aws-credentials action now ignores a thumbprint if one is given. On GitHub Enterprise Server the issuer is your server's host instead of token.actions.githubusercontent.com. Full setup: guides/iam/ci-oidc-github.md.
5. "Any role matching a name pattern"
Partial wildcards are not allowed in Principal. Put the pattern in a condition instead:
{
"Effect": "Allow",
"Principal": { "AWS": "*" },
"Action": "sts:AssumeRole",
"Condition": { "ArnLike": { "aws:PrincipalArn": "arn:aws:iam::<account-id>:role/<prefix>-*" } }
}
This form also survives deleting and recreating the named roles, because condition ARNs are not converted to unique IDs. Without the condition, "Principal": {"AWS": "*"} means everyone, including anonymous callers.
Known-bad
| What was written | Symptom | Fix |
|---|---|---|
"Principal": {"AWS": "arn:aws:iam::<acct>:role/deploy-*"} | Saving fails: Failed to update trust policy. Invalid principal in policy. (MalformedPolicyDocument from the API) | Use pattern 5 |
| A trust policy naming a role that was later deleted and recreated with the same name | AssumeRole now fails with … because no role trust policy allows the sts:AssumeRole action; the policy shows an ID starting AROA (or AIDA for a user) instead of the ARN | Edit the policy and re-enter the ARN; prefer pattern 5 for roles that get recreated |
"Service": "Lambda.amazonaws.com" (wrong case) or a guessed service principal | The service cannot assume the role, so the resource that uses it fails to create or to run (exact text varies by service; not collected) | Copy the exact principal from the service's documentation |
"Principal": {"AWS": "*"} with no condition | Works for you — and for everyone else, including anonymous callers | Name the principal, or add an aws:PrincipalArn / sts:ExternalId condition |
GitHub OIDC trust with only the aud condition, or sub as StringLike repo:<org>/<repo>:* | Works, but any branch, pull-request merge ref or environment in the repository can deploy with the role | Pin sub to a branch or a protected environment with StringEquals |
GitHub OIDC trust listing sts:AssumeRole instead of sts:AssumeRoleWithWebIdentity | The workflow's credential step fails to assume the role (exact text not collected) | Use the web identity action for a Federated OIDC principal |
| A role in a GovCloud (US) account trusting a standard-partition account, or the reverse | Invalid principal in policy | Roles cannot delegate across those partitions; keep each side in its own partition |
Which side is wrong?
A trust failure surfaces at the caller as an access denial on sts:AssumeRole. Read the end of the message:
because no role trust policy allows …orwith an explicit deny in the role trust policy— the role's side: fix the trust policy.because no identity-based policy allows the sts:AssumeRole action— the caller's side: the caller's own account must allowsts:AssumeRoleon the role ARN.- An SCP or permissions boundary named — the caller's organization or boundary caps
sts:AssumeRole; seereference/iam/errors.md.