Home › DevOps › AWS AccessDenied — Decode and Fix IAM Fast
Intermediate 5 min · September 23, 2026

AWS AccessDenied — Decode and Fix IAM Fast

AccessDenied means an explicit or missing allow.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
✓ Production
production tested
September 23, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 20 min
  • ✓Basic AWS IAM concepts
  • ✓Comfortable with AWS CLI commands
  • ✓Understanding of JSON policies
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • AccessDenied means no allow (missing policy) or an explicit deny (SCP, boundary, or deny statement) — decode first to learn which
  • Decode the encoded message with aws sts decode-authorization-message to get the exact action, resource, and context
  • An explicit deny from an SCP or permissions boundary beats any allow — check those before adding policies
  • Fix with least privilege: the single missing action on the narrowest resource, verified by replaying the call
✦ Definition~90s read
What is AWS AccessDenied Authorization Fix?

AccessDenied (and its siblings UnauthorizedOperation, Forbidden, and explicit-deny failures) means AWS's policy evaluation engine refused the request. The mechanism is a strict algorithm over all applicable policies: default-deny first (without an explicit allow, everything is denied), then any explicit deny anywhere overrides every allow.

★
Imagine an office where every door needs both your badge clearance AND the building rulebook to allow you.

The engine gathers the identity policy, the resource policy, the SCP chain, the permissions boundary, and any session policy, then applies: explicit deny wins, else explicit allow wins, else implicit deny. The error message tells you the action and resource that lost — never which policy cast the deciding vote.

The encoded authorization message is the diagnostic AWS hands you instead: a base64 blob containing the decision context — the evaluated action, resource ARN, principal, and the condition keys (aws:PrincipalTag, kms:ViaService, aws:SourceVpce) that mattered. Decoding it with sts decode-authorization-message converts 'AccessDenied' into 's3:PutObject on arn:aws:s3:::reports/q3.pdf denied, with kms:ViaService absent' — a sentence you can act on.

What AccessDenied is NOT: it's not SignatureDoesNotMatch (identity recognized but math failed), not InvalidClientTokenId (the key itself is bad), not a network or endpoint error (those fail before evaluation), and not eventually-consistent IAM propagation in most modern cases (that resolves in seconds and looks identical, so always wait 30s and retry once before investigating). AccessDenied specifically testifies that evaluation ran and refused — so the investigation targets policies and context, never keys, clocks, or regions.

Plain-English First

Imagine an office where every door needs both your badge clearance AND the building rulebook to allow you. Your badge opens labs (your IAM policy allows it), but the rulebook says 'no contractors on floor 3 after 6 PM' (an SCP deny) — and the rulebook always wins. 'AccessDenied' just means a door stayed shut; it never says which lock did it. Decoding the error asks security exactly which rule fired, so you fix that rule instead of randomly upgrading badges and wondering why nothing changes.

Your deploy fails with AccessDenied, the encoded message is a wall of base64, and the engineer's first instinct is to attach AdministratorAccess 'just to unblock' — which works, ships to production, and becomes a $47K incident six months later when those credentials leak. The error names what was denied but never which policy is responsible, so teams guess: they add S3FullAccess for a KMS problem, or loosen a bucket policy when an SCP is the actual wall.

AccessDenied strikes at the intersection of five policy layers: identity policies, resource policies, SCPs, permissions boundaries, and session policies. Any single explicit deny anywhere beats every allow everywhere — which is why adding permissions to a denied principal changes nothing, and why the fix requires finding the deny before granting the allow.

The trap is treating all denials as missing allows. Half of production AccessDenied cases involve an explicit deny (often an SCP nobody on the team remembers), where the correct fix is scoping an exception — not stapling on broader permissions that silently expand blast radius.

By the end of this article you'll decode authorization messages into exact actions and context keys, trace denials through CloudTrail, evaluate the five policy layers in order, and write least-privilege fixes you can verify by replaying the denied call.

The Evaluation Algorithm: Deny Beats Allow, Always

AWS evaluates every request against up to five policy layers and applies three rules in strict order: an explicit deny in any layer wins immediately; otherwise a single explicit allow anywhere permits; otherwise an implicit deny refuses. This ordering is the entire debugging strategy in one paragraph — it means adding allows can only fix implicit denials (missing permissions), never explicit ones (SCPs, boundaries, deny statements). The teams that internalize this stop escalating privileges on day one and start hunting denies instead.

Map each layer to who owns it and what it can do. Identity policies (attached to users, groups, roles) grant allows and occasional denies — your team's normal surface. Resource policies (bucket policies, key policies, trust policies) grant cross-account access and can also deny. SCPs (organization level) can only restrict — they never grant, only filter the maximum available. Permissions boundaries cap a single principal's maximum. Session policies narrow assumed-role sessions. A request needs an allow surviving every filter with no deny anywhere.

The practical consequence: when generous identity policies fail, the answer is never 'more allows' — it's a deny in a layer the team forgot. Check SCPs, boundaries, KMS key policies (S3 writes to KMS-encrypted buckets need key-policy allows too), and trust-policy conditions in that order. Each is a five-minute check that saves days of allow-stacking. Save every simulation command with its verdict in the incident ticket — the allow/deny table becomes the audit trail proving the fix was scoped, and the next engineer inherits evidence instead of folklore.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Evaluate all layers at once — the verdict names deny vs allow explicitly
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/backup-role \
  --action-names s3:PutObject kms:GenerateDataKey \
  --resource-arns arn:aws:s3:::reports/q3.pdf arn:aws:kms:us-east-1:123456789012:key/abcd \
  --query 'EvaluationResults[*].{action:EvalActionName,decision:EvalDecision,matched:MatchedStatements[*].SourcePolicyId}' \
  --output table
# decision=explicitDeny -> hunt the matched deny (SCP/boundary), stops here
# decision=implicitDeny -> genuinely missing allow, grant least privilege
# decision=allowed      -> policy fine, check conditions/context or propagation delay

# List the deny-capable layers most teams forget
aws organizations list-policies-for-target --target-id 123456789012 --filter SERVICE_CONTROL_POLICY
aws iam get-role --role-name backup-role --query 'Role.PermissionsBoundary'
⚠ Never Attach AdministratorAccess to Diagnose
Escalating to admin can't fix explicit denies (they still apply), masks the real cause, and the temporary grant reliably survives into production. Decode and simulate instead — they answer faster without expanding blast radius.
📊 Production Insight
Run the simulator in CI against your roles' critical actions. A policy refactor that flips allowed to implicitDeny fails the pipeline — catching the next AccessDenied before it ships instead of during the restore drill.
🎯 Key Takeaway
Explicit deny beats every allow. Simulate to classify deny versus missing-allow, then hunt the matched deny layer before granting anything.

Decode the Message: From Base64 to Actionable Sentence

The encoded authorization message looks like noise and reads like a verdict. It's a base64 (sometimes gzipped) JSON blob carrying the evaluation context: principal ARN, action, resource, and the condition keys AWS considered. Decoding needs only STS access with the same credentials — no IAM read permissions, no admin, no ticket to the security team. The engineer on call can run it in the first minute of any AccessDenied page.

Read the decoded output for three fields: the action-plus-resource pair (what was attempted), the context block (which condition keys were present or absent — the missing kms:ViaService in this article's incident was the whole diagnosis), and any matched-statement hints. Absent context keys are the classic reveal: policies demanding encryption, VPC endpoint, MFA, or tag conditions fail when callers simply don't send those attributes.

Make decoding muscle memory, not archaeology. Paste the message, decode, pretty-print, and read it before opening the IAM console. Half of all AccessDenied investigations end at this step with a sentence like 'PutObject denied because the call didn't specify KMS encryption' — a caller fix, not a policy fix. Store the decoded JSON in the incident ticket alongside the encoded original — future auditors and the next on-call inherit the full decision context instead of a base64 blob nobody can read.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Decode in the first minute — needs only STS, no admin, no tickets
aws sts decode-authorization-message \
  --encoded-message 'ENCODED_MESSAGE_PASTE_HERE' \
  --query DecodedMessage --output text | python3 -m json.tool

# Read three things in the output:
# 1. action + resource  -> 's3:PutObject on arn:aws:s3:::reports/q3.pdf'
# 2. context keys       -> which conditions were present vs ABSENT
#    e.g. missing 's3:x-amz-server-side-encryption: aws:kms' = caller fix
# 3. principal          -> confirm WHO was evaluated (often a surprise role)

# One-liner for pipelines: decode straight from a failed command's output
# aws s3api put-object ... 2>&1 | grep -o 'Encoded[^ ]*' > /tmp/enc.txt
# aws sts decode-authorization-message --encoded-message "$(cat /tmp/enc.txt)" --query DecodedMessage --output text
📊 Production Insight
The evaluated principal surprises teams regularly — an assumed session role, a federated mapping, or a service-linked role instead of the expected one. Confirm WHO before fixing WHAT, or your policy edit targets the wrong principal.
🎯 Key Takeaway
Decode first with the same credentials — action, resource, and absent context keys usually complete the diagnosis in one command.

SCPs and Boundaries: The Denies Nobody Remembers

Service Control Policies and permissions boundaries share one trait: they're set once by a security project, then forgotten by everyone who debugs daily IAM. SCPs attach to OUs and accounts, filtering the maximum permissions for everything inside — they grant nothing, only deny-shaped guardrails like 'no unencrypted S3 writes' or 'no leaving these regions.' Boundaries cap individual roles, commonly applied to delegated developer roles. Both produce explicit denies that survive AdministratorAccess, which is exactly why escalation 'mysteriously fails' and teams spiral.

SCP evaluation has quirks worth knowing. FullAWSAccess (the default) allows everything unless another SCP denies — removing it accidentally denies everything, a spectacular self-inflicted outage. Deny statements in any attached SCP apply; allows merely carve the ceiling. And SCP changes propagate in seconds but cached sessions can confuse — always test with fresh credentials after SCP edits.

Treat these layers as inventory, not mystery. List attached SCPs per OU quarterly, document each guardrail's intent and owner, and require security-team review for exceptions rather than silent allow-stacking elsewhere. When the simulator names an SCP deny, the correct fix is usually conforming the caller (encrypt with KMS, stay in-region) — the guardrail is working as designed.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Inventory the forgotten layers (quarterly review + incident use)
# SCPs affecting this account (walk up: account -> OU -> root)
aws organizations list-policies-for-target --target-id 123456789012 --filter SERVICE_CONTROL_POLICY
aws organizations describe-policy --policy-id p-deny-unencrypted 2>/dev/null

# Permissions boundary on the failing role
aws iam get-role --role-name backup-role --query 'Role.{boundary:PermissionsBoundary}'

# Which SCP statement actually fires? Simulate and read MatchedStatements
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/backup-role \
  --action-names s3:PutObject --resource-arns 'arn:aws:s3:::reports/*' \
  --query 'EvaluationResults[*].{decision:EvalDecision,matched:MatchedStatements}'
# matched[].SourcePolicyType == 'SCP' -> conform the caller, don't stack allows
📊 Production Insight
Detaching FullAWSAccess during 'cleanup' denies everything in the account instantly — the rare SCP action that breaks rather than guards. Guard that policy ID with change-control and a big warning comment.
🎯 Key Takeaway
SCPs and boundaries deny invisibly to IAM-console debuggers. Inventory them, simulate against them, and conform callers to working guardrails.

KMS, Trust Policies, and Cross-Service Denies

A large share of S3-flavored AccessDenied is actually KMS-flavored: writing to a KMS-encrypted bucket requires kms:GenerateDataKey (and reading needs kms:Decrypt) granted in the key policy, not just S3 permissions on the bucket. Teams grant S3FullAccess, watch PutObject still fail, and conclude IAM is broken — while the key policy quietly denies a principal it was never told to trust. The decoded message plus a kms-scoped simulation names this in seconds.

Trust policies are the second hiding spot: AssumeRole denials (with MFA, external-ID, or IP conditions) fail before any permission evaluation, producing AccessDenied on sts:AssumeRole rather than on the target action. Cross-account patterns add resource-policy counterparts — the bucket policy must allow the foreign principal AND the foreign identity policy must allow the action, with no deny on either side. Miss either half and both teams blame each other.

VPC endpoint and tag conditions complete the usual lineup: policies requiring aws:SourceVpce deny traffic that bypasses the endpoint, and attribute-based (tag) conditions deny when principals or resources lack the expected tags. Each manifests as 'worked from my laptop, fails from the VPC' or 'worked yesterday, fails after retagging' — patterns that scream context, not identity.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# KMS key policy: does it trust YOUR principal? (S3 allows are not enough)
aws kms get-key-policy --key-id abcd-1234 --policy-name default \
  --query Policy --output text | python3 -m json.tool | grep -B2 -A6 'backup-role\|GenerateDataKey'
# Missing -> add the role to the key policy, not more S3 permissions

# Trust policy on the role: do your assume conditions actually hold?
aws iam get-role --role-name backup-role --query 'Role.AssumeRolePolicyDocument'
# Check: MFA present? external ID correct? source IP in range?

# Simulate the FULL call including KMS (S3 + key actions together)
aws iam simulate-principal-policy \
  --policy-source-arn arn:aws:iam::123456789012:role/backup-role \
  --action-names s3:PutObject kms:GenerateDataKey kms:Decrypt \
  --resource-arns 'arn:aws:s3:::reports/*' 'arn:aws:kms:us-east-1:123456789012:key/abcd-1234'

# Replay the real call with encryption the guardrail demands
aws s3api put-object --bucket reports --key verify.txt --body /tmp/v.txt \
  --server-side-encryption aws:kms --ssekms-key-id abcd-1234
🔥S3FullAccess Never Fixes KMS Denies
Encrypted-bucket writes need key-policy allows (kms:GenerateDataKey) independent of S3 permissions. If PutObject fails with S3FullAccess attached, check the KMS key policy next — that's where the deny lives.
📊 Production Insight
Key policies have no simulator shortcut for cross-account principals in some paths — keep a canary writer per KMS-encrypted bucket that puts a tiny object hourly. Its failure is the earliest possible signal of key-policy drift.
🎯 Key Takeaway
Check key policies, trust policies, and VPC/tag conditions alongside identity policies. Simulate the full action set including KMS.

CloudTrail: Every Denial Leaves a Signed Confession

CloudTrail logs every denied API call with the principal, action, resource, error code, and source IP — a complete incident history your team can query instead of reconstruct. Group a day's AccessDenied events by role and action and the pattern usually confesses immediately: one role, one action, 100% denied starting at a deploy timestamp means a scoped policy gap or a fresh deny; scattered denies across actions after a rotation means credential or session confusion.

Use the right tool per trail shape. lookup-events covers the last 90 days for quick incident queries from the CLI. Lake queries (or Athena over the S3 archive) handle the long view — 'when did this denial first appear' across months, which separates fresh breaks from always-broken paths nobody exercised. Alert on the metric, not the anecdote: a CloudWatch alarm on AccessDenied counts per critical role pages on night one, not during the restore drill on day eleven.

Correlate denial start times with change history: SCP edits, boundary attachments, key-policy changes, deploy timestamps. The denial that begins within minutes of a change names its cause. This correlation habit turns 'mysterious AccessDenied' into 'the 14:32 SCP edit' in one query — and makes the fix a revert or a scoped exception instead of a week of archaeology.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Recent denials for one action (90-day lookup-events window)
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=PutObject \
  --start-time $(date -u -d '1 day ago' +%FT%TZ) --end-time $(date -u +%FT%TZ) \
  --query 'Events[*].CloudTrailEvent' --output text | grep -i accessdenied | head -10

# Pattern view: group denials by role + action (confesses in one glance)
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=PutObject \
  --start-time $(date -u -d '7 days ago' +%FT%TZ) --end-time $(date -u +%FT%TZ) \
  --query 'Events[*].CloudTrailEvent' --output text | \
  python3 -c "import sys,json; from collections import Counter; c=Counter((json.loads(l).get('userIdentity',{}).get('arn'), json.loads(l).get('errorCode')) for l in sys.stdin); print('\n'.join(f'{k} x{v}' for k,v in c.most_common(10)))"

# Correlate: what changed when denials started? (Config + deploy timestamps)
# aws configservice get-resource-config-history ... | grep -i policy
# Then: revert the change or scope the exception — not broader allows.
📊 Production Insight
Alarm on AccessDenied counts per backup/critical role with a one-cycle threshold. Silent multi-day denial runs (like the 11-day backup gap) become same-night pages for the price of one metric filter.
🎯 Key Takeaway
Group denials by principal and action to see the pattern, correlate start times with policy changes, and alarm on denial counts for critical roles.

Least-Privilege Fixes You Can Verify by Replay

The correct fix grants the single missing action on the narrowest resource with the required conditions — nothing more. decoded message says s3:PutObject on arn:aws:s3:::reports/* with KMS context? The fix is that action on that prefix, conditioned on the encryption the guardrail demands. Resist the bundle policies (S3FullAccess, PowerUser) — each wildcard permission is future blast radius sold for present convenience, and the $47K leaked-credential incidents always trace back to a 'temporary' broad grant.

Write the policy as code and simulate before attaching. A JSON statement with explicit Action, Resource, and Condition blocks, checked into version control with a comment naming the incident, beats console click-ops that nobody can review. Simulate the exact denied call (with context entries) and demand allowed; simulate adjacent actions (s3:DeleteBucket, kms:*wildcard) and demand deny — proving both that the hole closed and that no new one opened.

Close the loop by replaying the real call with the real principal, then watching CloudTrail for 24 hours. New allows propagate in seconds but cached sessions and edge cases surprise — the replay plus a denial-count alarm is the only honest definition of fixed. Document the decision (why this action, this resource, this condition) so the next engineer inherits understanding, not just JSON.

BASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Least-privilege fix as code: one action, narrow resource, required condition
cat > backup-put-fix.json <<'EOF'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "BackupWritesKMSOnly",
    "Effect": "Allow",
    "Action": ["s3:PutObject", "s3:AbortMultipartUpload"],
    "Resource": "arn:aws:s3:::reports/*",
    "Condition": {"StringEquals": {"s3:x-amz-server-side-encryption": "aws:kms"}}
  }]
}
EOF
aws iam put-role-policy --role-name backup-role --policy-name BackupWritesKMSOnly --policy-document file://backup-put-fix.json

# Prove it: the denied call now allowed, adjacent actions still denied
aws iam simulate-principal-policy --policy-source-arn arn:aws:iam::123456789012:role/backup-role \
  --action-names s3:PutObject s3:DeleteBucket --resource-arns 'arn:aws:s3:::reports/*' \
  --context-entries 'ContextKeyName=s3:x-amz-server-side-encryption,ContextKeyValues=aws:kms,ContextKeyType=string'
# Expect: PutObject=allowed, DeleteBucket=denied. Ship it.
📊 Production Insight
Condition-mirroring (policy demands what the guardrail demands) keeps layered defenses consistent. A fix that exempts the caller from encryption instead of conforming to it trades one incident for an audit finding.
🎯 Key Takeaway
Grant one action on the narrowest resource with matching conditions, simulate allow-plus-adjacent-deny, replay the real call, and document why.
● Production incidentPOST-MORTEMseverity: high

The SCP Nobody Remembered That Blocked Backups for 11 Days

Symptom
Starting after an SCP cleanup project, nightly database backups to S3 failed 100% with AccessDenied for 11 straight days — 264 failed backup jobs. The team attached S3FullAccess, then PowerUserAccess, then finally AdministratorAccess to the backup role; every escalation changed nothing. Backups silently stopped while monitoring (which checked job completion, not S3 object creation) stayed green, and the gap was discovered only during a restore drill that found the newest recoverable backup was 11 days old.
Assumption
The team assumed identity-policy gaps because that's where they'd always fixed AccessDenied before. Three engineers independently reviewed the role's policies, found them generous, and concluded the problem must be the bucket policy — spending 4 days tightening and loosening resource policies. Nobody examined SCPs because 'we don't use SCPs for anything real' — forgetting last year's security project had attached a guardrail SCP to the entire OU.
Root cause
A year-old SCP on the production OU denied s3:PutObject unless s3:x-amz-server-side-encryption was aws:kms — a well-meaning encryption guardrail. The new backup tooling wrote with AES256 (S3-managed) instead of KMS, tripping the explicit deny on every object. Identity allows (even AdministratorAccess) can't override an SCP deny by design. Decoding the message on day 11 showed the denial with the missing kms context key in one line; CloudTrail AccessDenied events had recorded the scp-evaluated deny for all 11 days, unqueried.
Fix
Three changes closed it. First, the backup tooling was switched to KMS encryption (matching the guardrail's intent) rather than excepting the role — the SCP was doing its job. Second, backup monitoring now verifies object creation in S3 (head-object on the expected key) instead of job exit codes, so silent backup death pages within one cycle. Third, the runbook's AccessDenied page was rewritten deny-first: decode the message, query CloudTrail for the denying policy type, and check SCPs and boundaries before touching identity policies — with a standing ban on AdministratorAccess as a diagnostic step.
Key lesson
  • Explicit denies beat all allows, so escalating permissions can't fix them. Eleven days of broader policies changed nothing because no allow overrides an SCP deny — decode first, escalate never.
  • Monitor the outcome, not the job. Backup jobs 'succeeded' while writing nothing for 11 days; asserting the S3 object exists would have paged on night one.
  • Forgotten guardrails outlive their authors. SCPs and boundaries set by departed engineers need ownership, documentation, and periodic review — or they become invisible walls.
Production debug guideFive ordered checks — decode, trail, layers, simulate, verify — that name the denying policy.5 entries
Symptom · 01
An encoded authorization message and no idea which policy denied
→
Fix
Decode it with the same principal's credentials — decoding requires no extra permissions beyond STS: aws sts decode-authorization-message --encoded-message '...paste...' --query DecodedMessage --output text | python3 -m json.tool. The decoded JSON names the action, resource ARN, principal, and the context keys evaluated (e.g. missing kms:ViaService). That single output converts a guess into a named action-plus-context you can trace through the policy layers below.
Symptom · 02
You need the denial's history: who denied what, when, and under which policy type
→
Fix
Query CloudTrail for AccessDenied events — every denied call is logged: aws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=PutObject --start-time $(date -u -d '1 day ago' +%FT%TZ) --end-time $(date -u +%FT%TZ) --query 'Events[*].CloudTrailEvent' --output text | grep -i 'accessdenied\|forbidden' | head -10. For Lake/Insights setups, filter errorCode=AccessDenied grouped by userIdentity.arn and eventName — the pattern (one role, one action, 100% denied) points straight at a missing allow or a scoped deny.
Symptom · 03
Identity policies look generous but the call still fails
→
Fix
Check the deny-capable layers that override allows: aws organizations list-policies-for-target --target-id <account-id> --filter SERVICE_CONTROL_POLICY (SCPs); aws iam get-role --role-name <role> --query 'Role.PermissionsBoundary' (boundaries); aws iam simulate-principal-policy --policy-source-arn <role-arn> --action-names s3:PutObject --resource-arns <arn> --query 'EvaluationResults[*].{decision:EvalDecision,matched:MatchedStatements}'. An explicitDeny verdict with MatchedStatements naming an SCP or boundary ends the search — no identity-policy edit can fix it.
Symptom · 04
The action seems allowed but condition keys might be failing silently
→
Fix
Simulate with the exact context the real call carries: aws iam simulate-principal-policy --policy-source-arn <role-arn> --action-names s3:PutObject --resource-arns arn:aws:s3:::reports/q3.pdf --context-entries 'ContextKeyName=s3:x-amz-server-side-encryption,ContextKeyValues=aws:kms,ContextKeyType=string' --query 'EvaluationResults[*].EvalDecision'. Then re-run WITHOUT the context entry — if allow becomes deny, the policy demands a condition your caller isn't sending. Fix the caller to send it (encrypt with KMS) rather than weakening the policy.
Symptom · 05
You applied a fix and need proof it works without waiting for the nightly job
→
Fix
Replay the exact denied call with the same principal and confirm, then lock it in: aws s3api put-object --bucket reports --key q3-verify.txt --body /tmp/v.txt --server-side-encryption aws:kms --ssekms-key-id <key-id> (replay); aws iam simulate-principal-policy ... --query EvalDecision (expect allowed); aws cloudtrail lookup-events filtered to the last 10 minutes showing no new AccessDenied for that pair. Only then close the incident — and keep the simulation command in the runbook for the next rotation.
AccessDenied Causes — How to Confirm and Fix Each
Root CauseHow to ConfirmFixPrevention
Missing allow on the identity policySimulator says implicitDeny; no deny in matched statementsGrant the single action on the narrowest resource as coded policyCI simulator checks on critical actions for every policy change
Explicit deny in SCP or permissions boundarySimulator says explicitDeny with SCP/boundary matched statementConform the caller to the guardrail (or get a scoped exception)Quarterly SCP inventory with owners; ban admin-escalation debugging
KMS key policy doesn't trust the principalS3 allows present but kms actions denied; key policy lacks the roleAdd the principal to the key policy with minimal kms actionsHourly canary writes per encrypted bucket; alert on failure
Trust policy or cross-account half missingAssumeRole denied, or one side allows while the other lacks itFix trust conditions (MFA/external ID) and mirror allows both sidesIntegration test cross-account paths on every trust-policy edit
Condition context absent (encryption, VPC, tags, MFA)Simulate with vs without context flips the verdictMake the caller send the required attribute; don't weaken the policyContract-test callers for required headers, tags, and encryption
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
aws iam simulate-principal-policy \The Evaluation Algorithm
aws sts decode-authorization-message \Decode the Message
aws organizations list-policies-for-target --target-id 123456789012 --filter SER...SCPs and Boundaries
aws kms get-key-policy --key-id abcd-1234 --policy-name default \KMS, Trust Policies, and Cross-Service Denies
aws cloudtrail lookup-events \CloudTrail
cat > backup-put-fix.json <<'EOF'Least-Privilege Fixes You Can Verify by Replay

Key takeaways

1
Evaluate deny-first
explicit deny beats every allow, so decode before granting.
2
sts decode-authorization-message converts noise into action, resource, and context.
3
SCPs and boundaries are the forgotten denies
simulate against them explicitly.
4
KMS key policies and trust policies hide beside S3 and IAM policies.
5
CloudTrail groups denials into patterns; alarm on counts per critical role.
6
Fix least-privilege as code, simulate allow-plus-adjacent-deny, replay to verify.

Common mistakes to avoid

5 patterns
×

Attaching AdministratorAccess to 'unblock' the deploy

Symptom
The incident unblocks (unless an SCP denies) but the broad grant survives into production and becomes the next breach's blast radius.
Fix
Decode and simulate instead — faster answers with zero privilege expansion, plus a standing ban on admin-as-diagnostic.
×

Stacking allows onto an explicit deny

Symptom
Days of broader policies change nothing because no allow overrides an SCP, boundary, or deny statement.
Fix
Classify with the simulator first; explicitDeny means hunt the denying layer and conform or except — never stack.
×

Fixing S3 policies for what is actually a KMS denial

Symptom
S3FullAccess attached, PutObject still denied, team concludes IAM is broken while the key policy sits unexamined.
Fix
Simulate s3 plus kms actions together; grant the principal in the key policy when kms actions are the denied ones.
×

Editing the policy for the wrong principal

Symptom
Fixes to the assumed role change nothing because the evaluated principal was a session role or federated mapping.
Fix
Read the principal ARN from the decoded message first, then edit policies for exactly that principal.
×

Monitoring job success instead of the protected outcome

Symptom
Backup jobs report green for 11 days while writing nothing, discovered only at restore time.
Fix
Assert the outcome (object exists, row written, message delivered) and alarm on AccessDenied counts per critical role.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
A call fails with AccessDenied despite the role having S3FullAccess. Wha...
Q02SENIOR
Explain why an explicit deny beats AdministratorAccess.
Q03SENIOR
PutObject to a KMS-encrypted bucket fails with S3 permissions granted. D...
Q04SENIOR
How do you distinguish a missing-allow from an explicit-deny without con...
Q05SENIOR
Design guardrails so teams can't pile up broad grants during incidents.
Q01 of 05JUNIOR

A call fails with AccessDenied despite the role having S3FullAccess. What's your first step?

ANSWER
Decode the authorization message and simulate — S3FullAccess rules out missing S3 allows, so suspect an explicit deny (SCP, boundary) or a KMS key-policy gap. Escalating further can't fix denies. The decoded context plus simulator verdict names the actual layer in minutes.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Why didn't AdministratorAccess fix my AccessDenied?
02
How long does a new IAM policy take to apply?
03
What's the difference between AccessDenied and SignatureDoesNotMatch?
04
Do I need access to the other account to debug cross-account denies?
05
Can I simulate policies without production credentials?
06
How do I stop incident-era broad grants from lingering?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Lessons pulled from things that broke in production.

Follow
✓ Verified
production tested
September 23, 2026
last updated
1,942
articles · all by Naren
🔥

That's Cloud. Mark it forged?

5 min read · try the examples if you haven't

←
Previous
Lambda Task Timed Out Fix
12 / 13 · Cloud
Next
Kafka CommitFailedException Fix
→