Most AWS privilege problems are not caused by a missing control but by policies nobody can reason about: nine policy types, a trust policy on every role and an evaluation order that turns small wildcards into account-wide access. This guide goes below the account structure and into the policy language itself: which policy types exist and what each can and cannot do, how AWS evaluates a request, how role trust policies, AssumeRole and role chaining work, how permissions boundaries and iam:PassRole contain delegation, and how to analyse policies with IAM Access Analyzer and the policy simulator. It is for cloud security engineers and reviewers who approve IAM changes. For the wider picture see the AWS Security Guide.
Key takeaways
- Only two policy types grant access. Identity-based and resource-based policies (including role trust policies) grant permissions; permissions boundaries, session policies, SCPs and RCPs only set ceilings.
- An explicit deny always wins. AWS checks every policy for a matching Deny first; otherwise the request needs an Allow that survives every ceiling.
- The trust policy is the front door of every role. Review who can call
sts:AssumeRoleas carefully as what the role can do. - Delegation needs two brakes. Let teams create roles only with a mandatory permissions boundary, and scope
iam:PassRoleto named roles and services, because passing a role is not logged as its own event. - Analyse with reasoning, not string matching. IAM Access Analyzer checks catch broad or deliberately hard-to-read policies that text search misses.
The policy types and what each can do
AWS IAM policies come in nine documented types. For each, ask whether it grants or only limits, and what it is attached to.
| Policy type | Attached to | Grants access? | Notes for reviewers |
|---|---|---|---|
| Identity-based (AWS managed, customer managed, inline) | IAM users, groups, roles | Yes | The root user cannot have them |
| Resource-based | Resources such as S3 buckets, KMS keys, SQS queues, and IAM roles (the trust policy) | Yes, including to other accounts | Always inline; has a Principal element |
| VPC endpoint policy | Interface or gateway endpoints | No, filters traffic through the endpoint | Default policy "allows full access" if you do not attach one |
| Permissions boundary | A single user or role (uses a managed policy) | No | Caps identity-based policies only |
| Session policy | An AssumeRole or federation session, passed as a parameter | No | One inline (Policy) and up to 10 managed (PolicyArns) |
| Service control policy (SCP) | Organisation root, OU or account | No | Limits principals in member accounts, including each root user |
| Resource control policy (RCP) | Organisation root, OU or account | No | Limits access to member account resources whoever the caller is |
| Access control list (ACL) | S3, AWS WAF, VPC | Yes, cross-account only | The only type not written in JSON; cannot control same-account access |
| AWS RAM resource share | Shared resources | Yes, to accounts, OUs or the organisation | Include RAM shares in access reviews |
AWS managed, customer managed and inline policies
- AWS managed policy: written and updated by AWS. "You cannot change the permissions defined in AWS managed policies", and when AWS updates one the change reaches every principal it is attached to. Job function policies such as
ReadOnlyAccessgrow as services are added: fine for people, wrong for workloads. - Customer managed policy: yours to write; it keeps up to five versions with one default, which gives a clean rollback path. The right container for least-privilege workload permissions.
- IAM inline policy: embedded in one identity and "deleted when you delete the identity". AWS's own rule of thumb: "If a policy could apply to more than one entity, it's better to use a managed policy." Flag inline policies in reviews: they are missed by managed policy inventories.
IAM policy evaluation logic: how AWS evaluates a request
AWS starts from an implicit deny: "By default, all requests are implicitly denied with the exception of the AWS account root user, which has full access." It then works through the applicable policies in a fixed order.
- Explicit deny: every applicable policy is checked for a matching
Deny; one match ends evaluation with Deny. - RCPs: must allow the action on a member account resource.
- SCPs: must allow it for a member account principal.
- Resource-based policies: an Allow can end evaluation with Allow for some principal types.
- Identity-based policies: otherwise an Allow is needed here.
- Permissions boundaries: must also allow it, where set.
- Session policies: must also allow it, where passed.
| Combination | Result | What catches people out |
|---|---|---|
| Identity-based and resource-based, same account | Union | A bucket or key policy can grant access the identity policy never mentions |
| Identity-based and permissions boundary | Intersection | A boundary does not limit what a same-account resource policy grants directly to an IAM user ARN |
| Identity-based, SCP and RCP | Intersection | The management account is outside both SCPs and RCPs |
| Identity-based and session policy | Intersection | A resource policy naming the role session ARN is not limited by an implicit deny in the boundary or session policy |
| Cross-account request | Both accounts must allow | "The request is allowed only if both evaluations return a decision of Allow." |
Explicit deny is the only reliable way to guarantee something cannot happen; an absent Allow can be supplied by another policy. Conditions matter as much as actions: a statement applies only when its Condition is true, so a key that is absent for some callers changes the outcome. AWS warns, for example, that aws:MultiFactorAuthPresent is absent for long-term credentials and recommends BoolIfExists in Deny statements.
Role trust policies, AssumeRole and cross-account roles
Every IAM role has two policies. The IAM role trust policy is "a required resource-based policy" that names the principals allowed to assume the role; the permissions policy is the identity-based policy that defines what the assumed IAM role can do. IAM role assumption happens through STS AssumeRole (sts:AssumeRole), which returns temporary credentials for a role session, identified by an assumed-role ARN such as arn:aws:sts::111122223333:assumed-role/examplerole/session. Workload roles such as an EC2 IAM role (delivered through an instance profile) are covered in the Cloud Workload Identity Guide.
- Who the principal is: a specific role ARN trusts only that principal. An account (
arn:aws:iam::111122223333:root) hands the decision to that account's identity policies, so anyone there withsts:AssumeRoleon*can use it. A wildcard principal without strong conditions trusts everyone. - Cross-account IAM role: the caller needs an identity-based Allow for
sts:AssumeRoleand the trust policy must allow the caller. Addaws:PrincipalOrgIDoraws:PrincipalOrgPathsconditions so only principals in your organisation, or a specific OU, qualify. - Third parties and the confused deputy: to stop a vendor being coerced into acting on another customer's account, require
sts:ExternalId, a value unique per customer and generated by the vendor, not by you. See the Third-Party Access Guide. - Service principals: where a policy trusts an AWS service, add
aws:SourceArnoraws:SourceAccountso the service acts only for your resource. - Session length: sessions run from 900 seconds to the role's maximum of 1 to 12 hours (default one hour). Keep privileged roles short.
IAM role chaining and the one-hour limit
Role chaining "is when you use a role to assume a second role", for example a CI role assuming a deployment role in each target account. It "limits your AWS Management Console, AWS CLI or AWS API role session to a maximum of one hour", whatever the second role's maximum, and a chained AssumeRole asking for longer fails. The security issue is attribution: each hop creates a new session, so the identity that started an assume role chain is easy to lose. Follow each AssumeRole event back to the first long-term or federated credential, and keep chains short and documented so an unexpected hop stands out.
Permissions boundaries and iam:PassRole
A permissions boundary (often called a permission boundary) sets "the maximum permissions that an identity-based policy can grant to an IAM entity" and grants nothing itself. Its main job is safe delegation: letting a team create roles for its workloads without creating one more powerful than itself.
- Write a boundary policy that allows the services the team owns and nothing in IAM, Organizations or security logging.
- Allow the team
iam:CreateRole,iam:PutRolePolicy,iam:AttachRolePolicyandiam:PutRolePermissionsBoundaryonly with the conditioniam:PermissionsBoundaryequal to that boundary's ARN, which is the pattern AWS documents for users. - Deny
iam:DeleteRolePermissionsBoundaryand any edit to the boundary policy itself, so the ceiling cannot be removed or widened. - Restrict role names to a prefix so the team cannot touch platform roles.
iam:PassRole is the other half. Services such as EC2, Lambda and CloudFormation accept a role ARN and check that the caller has iam:PassRole on it. AWS is explicit that "PassRole is not an API call. PassRole is a permission, meaning no CloudTrail logs are generated for IAM PassRole." Whoever can pass an administrator role to a new function is effectively an administrator. Scope the Resource to named role ARNs or a prefix, add iam:PassedToService to limit the service, and do not rely on resource tags, which AWS says "does not have reliable results" for PassRole. See the Cloud PAM and CIEM Guide.
Service control policies and resource control policies
Service control policies cap what principals in member accounts can do; resource control policies, launched by AWS on 13 November 2024 with support for S3, STS, KMS, SQS and Secrets Manager and since extended to many more services, cap what anyone can do to resources in member accounts. AWS's own split: "Use an SCP when you need to limit permissions of IAM principals within your organization's member accounts. Use an RCP when you need to restrict IAM principals that are external to your organization accounts making requests to access resources within your organization's member accounts."
- Neither grants: "an RCP never grants permissions", and nor does an SCP.
- Gaps: neither affects the management account or service-linked roles, and RCPs do not apply to AWS managed KMS keys.
- Data perimeter pattern: an RCP that denies S3, KMS, Secrets Manager or STS access when
aws:PrincipalOrgIDis not your organisation stops a misconfigured bucket or key policy granting outsiders access. Exempt AWS service principals withaws:PrincipalIsAWSService. - Testing: the policy simulator can include SCPs but "does not support resource control policies (RCPs)", so test RCPs on a low OU with CloudTrail review rather than simulation.
IAM condition keys that do the most work
| Condition key | Use it to | Watch for |
|---|---|---|
aws:PrincipalOrgID, aws:PrincipalOrgPaths | Limit resource and trust policies to your organisation or an OU | Service principals do not carry an organisation ID |
aws:SourceArn, aws:SourceAccount, aws:SourceOrgID | Prevent cross-service confused deputy when trusting a service principal | Missing on older bucket and key policies |
sts:ExternalId | Bind a third-party role to one customer relationship | Value must come from the vendor, not be chosen by you |
aws:SourceVpce | Require access through a specific VPC endpoint | Pairs with endpoint policies; see the network design |
aws:SourceIp | Restrict public source ranges | "Can only be used for public IP address ranges" and is absent through a VPC endpoint |
aws:MultiFactorAuthPresent | Require MFA for sensitive actions | Use BoolIfExists; plain Bool is "NOT RECOMMENDED" |
IAM policy size limits and quotas
When a team hits a size limit, the easy fix is a wildcard, which is how many overly permissive IAM policies start. Character limits "can't be increased"; white space is not counted.
| Quota | Value |
|---|---|
| Customer managed policy size | 6,144 characters |
| Inline policies, aggregate per entity | User 2,048; group 5,120; role 10,240 characters |
| Role trust policy length | 2,048 characters default, adjustable to 8,192 |
| Managed policies attached | Role 20 default, 25 maximum; user 10 default, 20 maximum; group 10 |
| Session policy | 2,048 characters for the JSON policy plus managed policy ARNs combined; up to 10 ARNs |
| Roles per account | 1,000 default, 10,000 maximum |
| Customer managed policies per account | 1,500 default, 10,000 maximum |
When a workload needs more, split permissions across customer managed policies by service rather than collapsing to service:*.
IAM policy analysis: finding overly permissive and obfuscated policies
Set the IAM Access Analyzer zone of trust to the organisation so findings mean "outside the organisation".
- External access analysis: resource policies, including role trust policies, granting access outside the zone of trust.
- Internal access analysis: which internal principals can reach selected critical resources such as S3 buckets and DynamoDB tables.
- Unused access analysis: unused roles, keys and passwords, and unused actions on active roles: the most direct way to shrink over-privileged IAM roles.
- Policy validation: grammar and best-practice checks returning security warnings, errors, general warnings and suggestions.
- Custom policy checks:
CheckNoNewAccess(does an updated policy grant new access compared with the current one),CheckAccessNotGranted(are named critical actions or resources excluded) andCheckNoPublicAccess(can a resource policy grant public access). Run them in the pipeline on every IAM change; checks for new access are charged. - Policy generation: a draft least-privilege policy built from CloudTrail activity.
The IAM policy simulator answers "would this principal be allowed this action" without making a real request. It tests identity-based policies, one boundary at a time, SCPs and resource policies you supply, but not RCPs, and results can differ for VPC endpoint policies, role chaining and multiple resource policies on one resource.
IAM policy obfuscation
An obfuscated IAM policy is one written to be hard to read on purpose, so that a reviewer or keyword alert misses a dangerous grant. The policy language allows forms that defeat text search: action names are case insensitive ("iam:ListAccessKeys is the same as IAM:listaccesskeys"), wildcards can sit inside action names (iam:*AccessKey*, or ? for one character), NotAction and NotResource grant everything except a list, and a grant can be split across several statements, policies or policy versions. The defence is to stop reviewing strings. Normalise policies before comparison (lower-case actions, expand wildcards against the service action list, resolve NotAction), flag any NotAction with Allow for manual review, and rely on reasoning-based checks such as CheckAccessNotGranted for your list of sensitive actions (iam:PassRole, iam:CreatePolicyVersion, iam:UpdateAssumeRolePolicy, sts:AssumeRole on *), which evaluate what a policy allows rather than how it is written.
Implementation roadmap
- Inventory: export every role with its policies, boundary and last-used data, and tag an owner. See the NHI Ownership Guide.
- Turn on analysis: create organisation-wide external and unused access analysers; triage external trust findings first.
- Fix trust policies: replace account-root and wildcard principals with specific ARNs, add organisation, external ID and source conditions, and shorten privileged sessions.
- Gate changes in the pipeline: run policy validation and
CheckNoNewAccessorCheckAccessNotGrantedon every pull request that touches IAM, starting in report-only mode for two to four weeks before blocking. - Right-size: replace broad AWS managed policies on workload roles with generated customer managed policies, one workload at a time, watching CloudTrail for
AccessDeniederrors. - Delegate and guard: introduce boundaries and scoped PassRole, then deny-based SCPs and an RCP data perimeter, tested on a low OU first.
Rollback: customer managed policy changes roll back by setting the previous version as default; boundaries and SCPs roll back by detaching them. Keep policy JSON in source control so a revert is a reviewed change.
Decision guidance
- Role or resource policy for cross-account access: use a role when the other account needs several actions across resources. Use a resource-based policy for a single bucket, key or queue, backed by an RCP.
- Boundary or SCP: SCPs suit organisation-wide rules that apply to everyone in an account. Boundaries suit rules for a subset of roles inside an account, such as those created by a delegated team.
- Managed or inline: default to customer managed. Use inline only when the permission must disappear with the identity.
- Small estates can rely on Access Analyzer, scoped PassRole and a few deny SCPs. Large estates need pipeline checks, boundaries, RCPs and entitlement analysis that follows role chains across accounts.
Detection and measurement
| Threat (ATT&CK v20) | CloudTrail events and signals | Primary control |
|---|---|---|
| T1098.003 Additional Cloud Roles | CreatePolicyVersion with default set, SetDefaultPolicyVersion, AttachRolePolicy, PutRolePolicy, AttachUserPolicy | Boundaries, SCP deny on IAM changes outside pipelines |
| T1098.003 via trust changes | UpdateAssumeRolePolicy, CreateRole with an external principal, new Access Analyzer external findings | Trust policy review, aws:PrincipalOrgID |
| T1078.004 Valid Accounts: Cloud Accounts | AssumeRole from unfamiliar accounts, networks or source identities; first use of dormant roles | Unused access removal, short sessions |
| Boundary removal | DeleteRolePermissionsBoundary, PutRolePermissionsBoundary with a different ARN | Deny on boundary changes |
| PassRole abuse | No PassRole event; watch CreateFunction, RunInstances and CreateStack that reference privileged role ARNs | Scoped PassRole, iam:PassedToService |
- External trust: active Access Analyzer external access findings on roles (target zero unexplained).
- Unused permissions: share of roles with unused access findings, and median days to close.
- Broad grants: count of policies with
Action: "*",NotActionwith Allow, or unscopediam:PassRole. - Pipeline coverage: share of IAM changes checked before deployment.
- Delegation safety: percentage of team-created roles carrying the required boundary (target 100).
Common failure patterns
- Over-permissive EC2 IAM role: an instance role that can list and read every bucket turns one server-side flaw into a data breach, as in the Capital One breach, where a firewall's role credentials were taken from the metadata service.
- Misconfigured IAM role trust: a cross-account role trusts a partner account's
:root, so any principal there with broad STS permissions can assume it. - Unscoped PassRole: developers with
iam:PassRoleon*attach an administrator role to a function they control, and no PassRole event exists to alert on. - Boundary without a lock: a boundary is required at creation but nothing denies removing it later.
- Long-lived keys with broad policies: a compromised key bound to an over-permissive IAM policy is abused fast: the AWS crypto-mining campaign had miners running within minutes. See the API Key Management Guide.
Framework mapping
| Control area | NIST SP 800-53 Rev. 5 | CIS Controls v8.1 | ISO/IEC 27001:2022 | ATT&CK |
|---|---|---|---|---|
| Least-privilege policies, boundaries, PassRole scoping | AC-6, AC-3 | 6.8 | A.5.15, A.8.2 | T1098.003 |
| Role inventory, ownership, unused access removal | AC-2 | 5.1, 5.3 | A.5.16, A.5.18 | T1078.004 |
| Trust policies and cross-account access | AC-3, AC-20 | 6.1 | A.5.19, A.8.3 | T1078.004 |
| Pipeline policy checks and change control | CM-3, CM-5 | 4.1 | A.8.32 | T1098.003 |
| CloudTrail monitoring of IAM changes | AU-6, AU-12 | 8.2, 8.11 | A.8.15, A.8.16 | T1098.003 |
Practitioner checklist
- Inventory every role with its trust policy, permissions, boundary, owner and last-used date.
- Replace wildcard and account-root principals in trust policies with specific ARNs plus organisation conditions.
- Require
sts:ExternalIdon third-party roles and source conditions on service principal grants. - Scope
iam:PassRoleto named roles withiam:PassedToService; never allow it on*. - Enforce a permissions boundary on delegated role creation and deny its removal.
- Run policy validation and custom policy checks on every IAM change in the pipeline.
- Review unused access findings monthly and remove unused roles and actions.
- Flag
NotActionwith Allow,Action: "*"and normalised sensitive actions for manual review. - Alert on policy version, trust policy and boundary changes outside the deployment pipeline.
Standards and references
- AWS: Policies and permissions in AWS Identity and Access Management
- AWS: How AWS enforcement code logic evaluates requests to allow or deny access
- AWS: Roles terms and concepts
- AWS: Permissions boundaries for IAM entities
- AWS: IAM and AWS STS quotas
- AWS: The confused deputy problem
- AWS: IAM Access Analyzer custom policy checks
- MITRE ATT&CK: T1098.003 Account Manipulation: Additional Cloud Roles
Related NHI Mgmt Group resources: AWS Security Guide · Cloud Workload Identity Guide · Cloud PAM and CIEM Guide · Multi-Cloud Identity Guide · Service Account Security Guide