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

Created 18 Oct 2026 · Modified 23 Oct 2026 · 4 versions

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 with aws sts decode-authorization-message, which needs sts: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:AssumeRole on the role in its own account.

4. Find the refusing policy

Use the policy type from step 1:

Named typeWhere it livesWho can change it
identity-basedpolicies attached to the principal (and its groups, for a user)the account's IAM administrators
resource-basedthe resource's own policy (bucket, secret, key, queue)the resource owner
role trustthe target role's trust policythe role's account
permissions boundarythe boundary attached to the principalwhoever set the boundary
sessionthe session policy passed when the credentials were issuedwhoever requested the credentials
service control (SCP)AWS Organizations, on the account and every OU above itorganization administrators
resource control (RCP)AWS Organizations, applied to the resource's accountorganization administrators
VPC endpointthe endpoint's policythe 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 role filter. Credential errors (ExpiredToken, InvalidClientTokenId) are in reference/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.

Read this document as markdown — the same bytes an agent receives.