Diagnose an AccessDenied from the error text to the policy
Seven steps from "is not authorized to perform" to the policy that refused it and the change that fixes it, without trial-and-error edits.
markdownguidev4JSHZ-DGYGstage: operatetrust: officialtrust: fieldservice: iamservice: organizationsinterface: cliinterface: sdkAccessDeniedtroubleshoot access deniedpolicy simulatorsimulate-principal-policycross-account
Diagnose an AccessDenied
Do not start by widening a policy. An explicit Deny overrides every Allow, so adding permissions fixes nothing when a Deny matched, and a broad Allow added to "see if it works" usually stays. Read the message, identify the one policy type that refused, and change only that.
1. Capture the exact message
Copy the full error as the caller received it: the principal ARN, the action, the resource ARN if present, and the final clause. The final clause is the diagnosis:
because no <type> policy allows the <action> action— implicit deny: nothing in that policy type granted it. You will add an Allow.with an explicit deny in a <type> policy— explicit deny: a Deny statement matched. You will narrow the Deny or meet its condition.with an explicit deny in a <type> policy: <policy ARN>— the same, and AWS has named the exact policy. Go straight to it.- No clause, or a bare
with an explicit deny— the service does not use the detailed format. Every policy type is a suspect. Encoded authorization failure message: <long string>(EC2 and some other services) — decode it first withaws sts decode-authorization-message, which needssts:DecodeAuthorizationMessage; the decoded JSON names the denied action and context (reference/ec2/errors.md).
If you don't have the message (a denial inside a service, a CI job, someone else's call), CloudTrail records it in errorCode / errorMessage: guides/cloudtrail/find-who-did-what.md.
Search the hub for the clause: docs {"command": "search", "query": "<the clause>", "scope": "reference"}. The matching entry in reference/iam/errors.md has the ranked causes for that policy type.
2. Confirm who is calling
The principal in the message is the identity that actually signed the request. If it is not the one you expected, stop here: the problem is credentials, not policy.
aws sts get-caller-identity --debug
The debug output shows which credential source was used. Fix the source using reference/sts/credential-sources.md, then retry before changing any policy.
3. Decide whether the request is cross-account
Compare the account ID in the principal ARN with the account ID in the resource ARN.
- Same account: an Allow in either the identity-based policy or the resource-based policy is enough, unless something denies.
- Different accounts: both must allow — the caller's identity policy in its account and the resource policy in the resource's account. Fixing one side never fixes the other. If the target is a role, the role's trust policy is the resource policy, and the caller also needs
sts:AssumeRoleon the role in its own account.
4. Find the refusing policy
Use the policy type from step 1:
| Named type | Where it lives | Who can change it |
|---|---|---|
| identity-based | policies attached to the principal (and its groups, for a user) | the account's IAM administrators |
| resource-based | the resource's own policy (bucket, secret, key, queue) | the resource owner |
| role trust | the target role's trust policy | the role's account |
| permissions boundary | the boundary attached to the principal | whoever set the boundary |
| session | the session policy passed when the credentials were issued | whoever requested the credentials |
| service control (SCP) | AWS Organizations, on the account and every OU above it | organization administrators |
| resource control (RCP) | AWS Organizations, applied to the resource's account | organization administrators |
| VPC endpoint | the endpoint's policy | the endpoint's owner |
If the message includes a policy ARN, read that policy. The ARN is only shown when the caller is in the same account or organization as the policy; if you are calling across organizations and get no ARN, you must read every policy of the named type in scope. Only one policy type is reported even when several refused, so after fixing one, retry and read the next message.
5. Read the statement against the request
For an implicit deny, look for an Allow and check, in order: the action (including wildcards), the resource ARN character by character, then every condition key. Condition key names are case-insensitive, so request keys that differ only in case can collide.
For an explicit deny, find the Deny that matches the action and check its conditions. Typical matches: an MFA requirement (aws:MultiFactorAuthPresent with BoolIfExists set to false also blocks long-term access keys), a required VPC endpoint (aws:SourceVpce), an allow-list keyed on aws:userid, or a Region or service guardrail in an SCP or RCP. Several condition keys in one Deny statement must all match for it to apply.
6. Check the decision before changing anything live
aws iam simulate-principal-policy \
--policy-source-arn <principal ARN from the message> \
--action-names <action> \
--resource-arns <resource ARN>
It evaluates the action without performing it. Know its blind spots: it does not fetch resource-based policies (pass one with --resource-policy), it does not support RCPs, and its result can differ from the live environment, so confirm with the real call afterwards. It reports SCP effects. The principal must be a user, group or role, not an assumed-role session.
7. Make the smallest change, then retry
- Implicit deny: add an Allow for exactly that action, on exactly that resource, in the policy type that was named.
- Explicit deny: narrow the Deny, add an exception condition for the principal that needs access, or meet the condition (sign in with MFA, route through the required endpoint).
- SCP, RCP, or another account's policy: you cannot fix this yourself. Hand your human the message, the policy type, the policy ARN if given, and the exact change needed, addressed to the owner from the table in step 4.
Retry the original call. A new denial naming a different policy type means the first fix worked and another layer also refuses; return to step 1 with the new message.
If you are stuck
- The message matches no entry: search the hub with the exact text and no
rolefilter. Credential errors (ExpiredToken,InvalidClientTokenId) are inreference/sts/errors.md. - You signed the request yourself rather than through an SDK: a bad signature can also produce a denial; switch to the SDK to rule it out.
- The evaluation model itself is unclear:
reference/iam/policy-evaluation.md. - A console step in a fix does not match what your human sees:
guides/when-the-console-has-changed.md.