What it means
Every AWS call a Lambda function makes is signed with credentials from its execution role, and every one of those calls is evaluated against IAM before it does anything. AccessDeniedException is that evaluation returning no. It is the most mechanical error in this index, and also the most generously documented at the moment it occurs: unlike most failures, the message contains everything needed to fix it.
Three fields carry the whole diagnosis. The User: ARN is the assumed-role form — arn:aws:sts::<account>:assumed-role/<role-name>/<session> — where the session name is your function's name. That confirms which role was actually used, which matters whenever code assumes a second role for cross-account work. The action after to perform: is the IAM action name, which frequently differs from the SDK method you called. The on resource: ARN is what was touched, and comparing it against the policy is where most of these are solved.
The final clause is the part most often skipped and the one that changes the fix. "because no identity-based policy allows the action" means nothing granted the permission, and adding it will work. "with an explicit deny" — in an identity policy, a resource policy, a permissions boundary, or a service control policy — means something is actively refusing, and an explicit deny always beats an allow. In that second case, adding the permission to the role accomplishes nothing at all, and knowing which of the two you have before editing a policy saves a deploy cycle.
Two ARN mismatches account for a striking share of real occurrences. arn:aws:s3:::bucket and arn:aws:s3:::bucket/* are different resources: the first is the bucket, the second is the objects in it, and a policy granting one does not grant the other. Similarly a DynamoDB table's ARN does not cover its global secondary indexes, which need ${TableArn}/index/* listed separately. Both produce a denial whose ARN looks, at a glance, exactly like the one in the policy.
CloudWatch’s Errors metric: Whether CloudWatch’s Errors metric counts this depends on whether the error escaped your handler, which the log line alone does not settle. An AccessDeniedException that escapes the handler fails the invocation and is counted; one your code catches and turns into a 403 is not. LogStitch classified the example below from its log level rather than an extracted type, so it reports the failure as caught — check whether your own code actually handles it.
What it looks like in CloudWatch
This is the shape the failure arrives in: the lines of one invocation scattered among everything else the log group received at the same moment.
CloudWatch Logs Before
5 raw lines, in the order CloudWatch delivered them.
LogStitch After
The same lines, grouped into the invocation they belong to.
Error9a17c3e5-0b48-4d61-8c29-4f7e1a5b8d30
- 14:26:03.118PLAT
START RequestId: 9a17c3e5-0b48-4d61-8c29-4f7e1a5b8d30 Version: $LATEST
- 14:26:03.122INFO
INFO Order accepted order_id=ord_44718
- 14:26:03.288ERROR
ERROR AccessDeniedException: User: arn:aws:sts::111122223333:assumed-role/orders-fn-role/orders-fn is not authorized to perform: dynamodb:PutItem on resource: arn:aws:dynamodb:us-east-1:111122223333:table/orders because no identity-based policy allows the dynamodb:PutItem action
- 14:26:03.402PLAT
END RequestId: 9a17c3e5-0b48-4d61-8c29-4f7e1a5b8d30
- 14:26:03.402PLAT
REPORT RequestId: 9a17c3e5-0b48-4d61-8c29-4f7e1a5b8d30 Duration: 284.11 ms Billed Duration: 285 ms Memory Size: 256 MB Max Memory Used: 91 MB Status: error
The panel on the right is generated by running the excerpt on the left through the same parser that powers the free web stitcher — it is what the tool actually produces for this input, not an illustration of it.
How to confirm it from the logs
Read the three fields the message gives you. The User: is the assumed-role ARN of your function's execution role, in the form arn:aws:sts::<account>:assumed-role/<role>/<function>. The action after to perform: is the exact IAM action name, which is often not the API call name. The on resource: ARN is what the request touched.
The tail of the message matters as much as the head. "because no identity-based policy allows the action" means the permission is simply absent. "with an explicit deny in an identity-based policy" or "in a resource-based policy" means something is actively denying it — a permissions boundary or an SCP — and adding the permission to the role will change nothing.
Causes, most likely first
The action is missing from the execution role
Compare the action in the message against the role's policies. The action name is often not what you would guess from the SDK method — writing to DynamoDB with a document client still requires dynamodb:PutItem, and an S3 upload may need s3:PutObject plus s3:PutObjectAcl.
The resource ARN in the policy does not cover the one being accessed
Compare the ARN in the message character by character against the policy's Resource. A bucket ARN and the objects inside it are two different ARNs — arn:aws:s3:::bucket does not grant access to arn:aws:s3:::bucket/*. A DynamoDB index needs its own ARN alongside the table's.
A resource-based policy or KMS key policy is the real blocker
Check whether the resource has a policy of its own. A cross-account bucket, an SQS queue, or a KMS-encrypted resource must permit your role from its side as well; the identity policy granting the action is necessary but not sufficient.
A permissions boundary or SCP denies it regardless of the role
Read the reason at the end of the message. An explicit deny cannot be overridden by adding an allow, so if the wording mentions a boundary or an organizational policy, the fix is at that level rather than on the function's role.
The function assumed a different role than you expect
Check the role name inside the User: ARN against the function's configured execution role. If the code calls sts:AssumeRole for cross-account work, the denial may concern the assumed role rather than the execution role.
Fixes
Grant exactly the action named, on exactly the resource named
Copy the action and the ARN out of the error message rather than guessing at them. Note that a bucket and its contents are separate ARNs, and that list operations act on the bucket while object operations act on the objects.
yamlPolicies:
- Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- dynamodb:PutItem
- dynamodb:GetItem
Resource:
- !GetAtt OrdersTable.Arn
# An index needs its own ARN.
- !Sub "${OrdersTable.Arn}/index/*"
Reproduce the denial with the policy simulator before redeploying
The simulator answers "would this role be allowed?" without a deploy cycle, and it reports which statement decided the outcome — which is the fastest way to find an explicit deny.
bashaws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::111122223333:role/orders-fn-role \
--action-names dynamodb:PutItem \
--resource-arns arn:aws:dynamodb:us-east-1:111122223333:table/orders
Check the resource's own policy for cross-account access
Where the resource lives in another account or is encrypted with a customer-managed key, both sides must allow the call. Read the resource policy as well as the role.
bashaws s3api get-bucket-policy --bucket partner-exports
aws kms get-key-policy --key-id alias/orders --policy-name default
Also seen as
The same underlying failure, worded differently by a different runtime, SDK version, or logging layer. All of these land here — there is no separate page for each phrasing.
Related errors
Errors that show up alongside this one, or that people mistake for it.
References
LogStitch finds this automatically, across every invocation in your account.
Paste a log excerpt into the free web stitcher and see it grouped, classified, and measured in your browser — nothing is uploaded. Or run the Mac app against your own AWS profiles and get the same view over every function you own.