IAM Policies: Trust Policy vs Permission Policy, Conditions, and When Managed Policies Break
IAM has two distinct policy types that serve completely different purposes. Trust policies control who can assume a role. Permission policies control what that role can do. Mixing them up — or failing to understand condition keys — produces access errors that are hard to debug.
Two completely separate questions
IAM roles have two separate authorization questions:
- Who can assume this role? (Trust policy)
- What can this role do? (Permission policy)
These are answered by different policy documents, attached in different places, and evaluated at different times. Confusing them is the most common source of IAM debugging time.
Trust policy vs permission policy
ConceptAWS IAMTrust policies answer 'who can assume this role'. Permission policies answer 'what can the role do'. Both must be satisfied for an action to succeed on an assumed role.
Prerequisites
- AWS IAM roles and users
- STS AssumeRole
- resource-based vs identity-based policies
Key Points
- Trust policy: attached to a role, defines Principal — who can call sts:AssumeRole on this role.
- Permission policy: attached to a role, defines what actions the role's credentials can perform.
- A principal must be trusted by the role's trust policy to assume it, regardless of their own permissions.
- Each role has exactly one trust policy and can have multiple permission policies.
Trust policy anatomy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
This trust policy allows the Lambda service to assume the role. Without it, your Lambda function cannot use the role's credentials even if the permission policy grants the right actions.
Common principals:
"Service": "lambda.amazonaws.com"— Lambda functions"Service": "ecs-tasks.amazonaws.com"— ECS tasks"Service": "ec2.amazonaws.com"— EC2 instance profiles"AWS": "arn:aws:iam::123456789012:role/other-role"— another IAM role (cross-role assumption)"AWS": "arn:aws:iam::987654321098:root"— another AWS account (cross-account)
Permission policy anatomy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/*"
},
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::my-bucket"
}
]
}
Note that ListBucket applies to the bucket resource (arn:aws:s3:::my-bucket) while GetObject/PutObject apply to objects within the bucket (arn:aws:s3:::my-bucket/*). A common mistake is using my-bucket/* for ListBucket — it silently does nothing because list operations are on the bucket, not its contents.
Condition keys: restricting access by context
Conditions allow policy statements to only apply when certain context conditions are met:
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::reports-bucket/*",
"Condition": {
"StringEquals": {
"s3:prefix": ["reports/${aws:userid}/"]
}
}
}
This grants each IAM user access only to their own prefix in the bucket. ${aws:userid} is an IAM policy variable — it expands to the caller's user ID at evaluation time.
Useful condition keys:
aws:SourceIp— restrict to specific IP ranges (CIDR)aws:RequestedRegion— restrict which regions can be accessedaws:MultiFactorAuthPresent— require MFA for sensitive actionsaws:PrincipalTag/team— restrict based on tags on the caller's principal
{
"Effect": "Allow",
"Action": "ec2:TerminateInstances",
"Resource": "*",
"Condition": {
"Bool": {
"aws:MultiFactorAuthPresent": "true"
}
}
}
This allows ec2:TerminateInstances only when the session was authenticated with MFA.
💡Managed policies vs inline policies: when to use which
AWS managed policies: created and maintained by AWS (e.g., AmazonS3ReadOnlyAccess). Easy to attach, automatically updated for new services. The downside: they are broad. AdministratorAccess grants every action on every resource — too permissive for production roles.
Customer managed policies: you create and maintain them. Reusable across multiple roles. Version-controlled if created via Terraform/CDK. Recommended for production — you control the permissions precisely.
Inline policies: embedded directly in a role, user, or group. Cannot be reused. Appropriate when the policy is tightly coupled to a specific role and should not be reattached elsewhere by mistake.
The risk of managed policies at scale: AWS updates them. If a new action is added to an existing managed policy, all roles with that policy get the new permission immediately, without any change in your infrastructure code. For security-sensitive roles, prefer customer managed policies where you control what permissions are added.
Resource-based policies vs identity-based policies
Not all access control in AWS goes through IAM roles. Some services support resource-based policies — attached to the resource itself (S3 bucket policy, SQS queue policy, KMS key policy).
// S3 bucket policy (resource-based): grants a specific role access
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:role/my-lambda-role"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::private-bucket/*"
}
For access to succeed within the same account: either the identity-based policy OR the resource-based policy can grant access (either is sufficient).
For cross-account access: BOTH the identity-based policy on the caller AND the resource-based policy on the resource must allow the action.
Same account: identity policy OR resource policy → access
Cross-account: identity policy AND resource policy → access
This is why a cross-account role assumption may fail even when the calling role's permission policy allows the action: the target account's resource-based policy (e.g., S3 bucket policy) may not grant access to the external account.
A Lambda function in Account A tries to write to an S3 bucket in Account B. The Lambda's execution role in Account A has s3:PutObject in its permission policy for the target bucket. The Lambda still receives AccessDenied. What is the most likely missing piece?
mediumThe Lambda's execution role trust policy is correct (allows lambda.amazonaws.com to assume it). The permission policy explicitly allows s3:PutObject on the bucket ARN.
AThe Lambda execution role needs sts:AssumeRole to access cross-account resources
Incorrect.sts:AssumeRole is for assuming other roles, not for directly calling S3. The Lambda is using its own execution role credentials to call S3 directly.BThe S3 bucket in Account B needs a bucket policy that grants Account A's Lambda role access
Correct!Cross-account access requires both sides to allow the operation. The Lambda's identity-based policy in Account A allows s3:PutObject. But Account B also needs a resource-based policy (S3 bucket policy) explicitly granting access to the Lambda role ARN from Account A. Without the bucket policy, Account B implicitly denies all cross-account access.CThe S3 bucket must be in the same region as the Lambda function
Incorrect.S3 bucket policy and IAM permissions control access. Region is not a factor for S3 access.DThe Lambda execution role needs to be in Account B to access Account B's S3 bucket
Incorrect.Cross-account access is specifically designed for this case — Account A resources accessing Account B resources. The role stays in Account A.
Hint:In cross-account access, remember the rule: both identity policy AND resource policy must allow the action.