---
cuppy_hub: aws
cuppy_path: patterns/iam/trust-policies.md
cuppy_section: docs
cuppy_doc: VZNY-HXEY
cuppy_doc_uuid: 3c09ef09-4b61-4d82-b56a-bf5d064a776e
cuppy_permalink: https://cuppy.io/aws/d/VZNY-HXEY
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/patterns/iam/trust-policies
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: d534851838b3704a28f9582ff13d7672a8d24c4c983f5c4c36807cc70dd3d82e
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# 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, …)

```json
{
  "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

```json
{
  "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:

```json
"Condition": { "Bool": { "aws:MultiFactorAuthPresent": "true" } }
```

The caller must then pass `SerialNumber` and `TokenCode`.

### 3. A role a third party assumes into your account

```json
{
  "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

```json
{
  "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:

```json
{
  "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 …` 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`.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
