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

Created 18 Oct 2026 · Modified 23 Oct 2026 · 4 versions

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 writtenSymptomFix
"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 nameAssumeRole 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 ARNEdit 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 principalThe 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 conditionWorks for you — and for everyone else, including anonymous callersName 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 rolePin sub to a branch or a protected environment with StringEquals
GitHub OIDC trust listing sts:AssumeRole instead of sts:AssumeRoleWithWebIdentityThe 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 reverseInvalid principal in policyRoles 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 … or with 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 allow sts:AssumeRole on the role ARN.
  • An SCP or permissions boundary named — the caller's organization or boundary caps sts:AssumeRole; see reference/iam/errors.md.

Read this document as markdown — the same bytes an agent receives.