Lambda AccessDenied on SQS: Fix the Queue Policy
If your Lambda function is getting AccessDenied on an Amazon SQS queue, the fix almost never starts on the Lambda side — it starts with the SQS queue's own resource policy, called the access policy. In the same AWS account, either the Lambda execution role's IAM policy or the queue's access policy can grant the permission on its own; across accounts, both policies must say yes, and an explicit Deny in either one beats every Allow you've written. Here's the part almost nobody expects: most same-account Lambda functions don't need a queue policy statement at all.
Jake called on a Tuesday afternoon, which is never a good sign, because Tuesdays are when he tries to "quickly automate something" between customers. He'd wired up a Lambda function to pull warranty-claim messages off an SQS queue and file them into his repair tracker. It worked in testing. Then he pointed it at the "real" queue — one another contractor had set up in a separate AWS account for him — and every single invocation came back with the same wall of red text: AccessDenied.
"I gave the Lambda role AmazonSQSFullAccess," he said. "Full access. It says full access right there in the policy name. How is this still denied?"
Ethan didn't open the IAM console yet. "Because 'full access' only ever describes one side of the conversation," he said. "Before we touch a single IAM policy, tell me one thing — is that queue actually sitting in your AWS account, or did your contractor create it in his?"
Quick diagnostic: which side is actually denying you?
Ethan's first move with Jake wasn't to open the IAM console. He leaned back and asked one question before touching anything else. "Before we rewrite a single policy — is that queue even sitting in your AWS account, or somebody else's?"
That single question splits almost every SQS "Access Denied from Lambda" case into two completely different problems, because Amazon SQS evaluates access differently depending on the answer. Before touching any policy, work out which bucket you're in:
- Same account, straightforward Lambda-to-SQS. Your Lambda function's execution role and the queue live under the same 12-digit AWS account number. This is the most common setup — an event source mapping (Lambda polling the queue) or your own code calling
SendMessage/ReceiveMessagewith the AWS SDK. - Different accounts. The queue belongs to one AWS account; the Lambda function's execution role belongs to another. This is Jake's exact situation — his contractor's queue, his Lambda function.
- A third-party AWS service is the one calling SQS — Amazon S3 sending event notifications, Amazon SNS fanning a topic out to a queue, or Amazon EventBridge routing an event — and your Lambda function only reads what lands there. The denial here isn't about Lambda at all; it's about whether that service is allowed to write to the queue in the first place.
An IAM user or role's permissions and an SQS queue's own permissions are evaluated together, but not identically, in each of those three cases. Get the mental model right first, and the JSON you need to write follows almost automatically.
How Amazon SQS actually decides Allow or Deny
Every SQS API call — SendMessage, ReceiveMessage, DeleteMessage, GetQueueAttributes, all of it — passes through two possible policies before AWS decides whether to run it:
The identity-based policy
This is the IAM policy attached to whoever (or whatever) is making the call — in your case, the Lambda function's execution role. It's called "identity-based" because it's attached to an identity, not a resource. Everyone who has ever attached a permissions policy to an IAM role has written one of these.
The resource-based policy (the queue's "access policy")
This one lives on the SQS queue itself, not on any IAM role. AWS calls it the SQS access policy. You'll find it in the SQS console under the queue's Access policy tab, written as the same kind of JSON document as an IAM policy, just attached to the queue instead of a user or role. A blank access policy isn't "no permissions" — SQS treats an empty access policy as allowing the queue owner's account full access, and everyone else gets nothing unless a statement says otherwise.
Amazon SQS's own documentation lays out exactly how those two policies combine, and it depends entirely on whether the caller is in the same AWS account as the queue:
| IAM (identity) policy | SQS queue policy | Result — same account |
|---|---|---|
| Allow | Allow | Allowed |
| Allow | Neither | Allowed |
| Allow | Deny | Denied |
| Neither | Allow | Allowed |
| Neither | Neither | Denied (implicit) |
| Deny | Anything | Denied |
Read that middle row twice, because it's the whole story for Jake's phone shop and for most readers here: in the same account, an Allow on the IAM role by itself is enough. An empty, default queue policy does not block you. This is exactly why so many people fix their IAM policy, watch it keep failing, and never think to check the queue — because in a huge number of setups, the queue policy genuinely isn't the problem.
🕐 The one line that changes everything
- Same account: a statement to allow access is required in either the IAM policy or the queue policy — not both.
- Different accounts: a statement to allow access is required in both the IAM policy and the queue policy.
- What that means for you: if Jake's Lambda role and queue were in the same account, his "full access" IAM policy would already have worked. Because the queue lives in a different account, the IAM policy was never going to be enough on its own — no matter how generous it was.
Same-account Lambda: do you even need a queue policy?
Usually, no. If your Lambda function and the queue are in the same AWS account, the standard path is: attach an IAM policy to the Lambda execution role that grants the SQS actions your code needs, scoped to that queue's ARN. That's it. No queue policy statement required, because the table above shows an Allow on either side is sufficient.
"People treat editing the queue policy like it's mandatory homework," Ethan told him. "It isn't. It's a tool for when the requester isn't in your account, or when a service like S3 is the one knocking. Most of the time you're just adding a second file to keep in sync for no reason."
That's also why a policy named AmazonSQSFullAccess is a bad default to reach for even when it technically works — it's an identity-based policy with no resource scoping at all, so it grants every SQS action on every queue in the account. It's not wrong in the same-account case; it's just far wider than almost any single Lambda function needs, and it hides the fact that the function's real permissions requirement is usually four or five specific actions on one queue.
🙋♂️ Jake's Reality Check
"So why does every tutorial I've read tell me to edit the queue's access policy, then?"
Because a lot of tutorials skip the account-boundary question entirely, or they're written for a case where another AWS service — not Lambda directly — needs to reach the queue. If your own Lambda code is polling or sending to a same-account queue, editing the IAM role is the whole fix. Save the queue policy edits for the cases later in this post where they're actually required.
For a Lambda function whose event source mapping polls an SQS queue automatically, AWS ships a managed policy built for exactly this: AWSLambdaSQSQueueExecutionRole. It grants receive-message, delete-message, and read-attribute access to SQS queues, plus permission to write logs to CloudWatch. If you're using the console's built-in "Add trigger" flow to connect a queue to a function, attaching this managed policy to the execution role — or an inline policy with the same actions scoped to your queue's ARN — covers it.
A minimal same-account IAM policy
Attach this to the Lambda execution role, replacing the region, account ID, and queue name:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:GetQueueAttributes",
"sqs:ChangeMessageVisibility"
],
"Resource": "arn:aws:sqs:us-east-1:111122223333:claims-queue"
}
]
}
If your function also sends messages (say, to a different queue after processing), add sqs:SendMessage and sqs:GetQueueUrl for that queue's ARN as a second statement. Scoping the Resource to one queue ARN instead of "*" is the difference between a policy that fails safely and one that quietly lets a typo in your code touch a queue it was never meant to.
- Confirm the account match. Run
aws sts get-caller-identityfrom a context that assumes the Lambda execution role, or check the role's ARN in the IAM console, and compare the account ID against the queue's ARN. If they match, you're in the same-account case. - Open the execution role in IAM. From the Lambda console, go to Configuration → Permissions, and click the role name — this opens it directly in IAM.
- Attach or edit the SQS permissions. Either attach
AWSLambdaSQSQueueExecutionRolefor the standard polling case, or add an inline policy like the one above for custom actions. - Scope the Resource ARN exactly. Copy the queue's ARN from the SQS console's queue details page — don't retype it, one wrong digit in the account ID silently breaks the match.
- Check the queue policy isn't explicitly denying you. Even in the same account, an explicit Deny on the queue policy overrides your IAM Allow. Open the queue's Access policy tab and look for any
"Effect": "Deny"statement with a broadPrincipal. - Re-test the specific action that failed — not just any SQS call. If
ReceiveMessagewas denied butSendMessagewasn't, your policy needs the receive-side actions specifically; a partial fix that only adds send permissions won't touch the real error.
Cross-account Lambda: now the queue policy is mandatory
This is Jake's exact problem, and it's genuinely common: the queue was set up by a contractor, a partner team, or a different business unit under its own AWS account, and your Lambda function needs to read from it. Amazon SQS supports this — a Lambda event source mapping can point at a queue in a different account, as long as the queue is in the same AWS Region as the function — but it only works if the queue's access policy explicitly names your Lambda execution role's ARN.
Ethan was blunt with Jake about where to spend the next ten minutes. "Every minute you spend staring at your own IAM role is a minute you're not spending on the one file that actually matters here — the queue policy sitting in your contractor's account. Your side was never going to be the problem."
✅ Why this is the one to fix first
In the cross-account row of the decision table, an IAM Allow with no matching queue-policy statement produces an implicit deny — same outcome as if you'd written nothing at all. There is no version of a cross-account fix that skips the queue policy. Start there, every time.
The queue policy that Account B (queue owner) needs to add
The queue owner adds this to the queue's access policy, naming the exact execution role ARN of the Lambda function in the other account:
{
"Version": "2012-10-17",
"Id": "Queue1_Policy_UUID",
"Statement": [
{
"Sid": "AllowCrossAccountLambdaRole",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111122223333:role/cross-account-lambda-sqs-role"
},
"Action": [
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:GetQueueAttributes",
"sqs:ChangeMessageVisibility"
],
"Resource": "arn:aws:sqs:us-east-1:444455556666:LambdaCrossAccountQueue"
}
]
}
111122223333 is the account ID where the Lambda function lives; 444455556666 is the account ID that owns the queue. Note the Principal is the execution role's ARN, not the Lambda function's ARN — SQS doesn't know or care what invoked the role, only which role made the call.
- In the Lambda account, create (or reuse) an execution role with the
AWSLambdaSQSQueueExecutionRolemanaged policy attached, so the function can read from SQS and write CloudWatch Logs. Note this role's full ARN — you'll paste it into the other account's queue policy. - In the queue-owner account, open the queue's Access policy tab in the SQS console and switch to the advanced JSON editor.
- Paste a statement naming the Lambda role's ARN as the Principal, scoped to the specific SQS actions the function needs — avoid
"sqs:*"unless you genuinely want the remote role to manage the queue itself, not just read from it. - Back in the Lambda account, create the event source mapping pointing at the full ARN of the queue in the other account, using the AWS CLI or your infrastructure-as-code tool — the console's "Add trigger" flow does not currently support selecting a queue outside the caller's own account, so this step has to happen through the API or CLI.
- Confirm the Region matches. Cross-account SQS event source mappings still require the queue and the function to sit in the same AWS Region — cross-account is supported, cross-Region is not.
⚠️ What this actually breaks
A cross-account queue policy that grants "sqs:*" to your Lambda role isn't just "generous" — it lets that role purge the queue, delete it, or rewrite the queue's own policy, from an account the queue owner doesn't control. Scope the Action array to only what the function does. If it only consumes messages, it never needs sqs:SendMessage, sqs:PurgeQueue, or sqs:SetQueueAttributes.
When another AWS service — not Lambda — needs the queue policy
Ethan's second question to Jake was: "Does anything besides your Lambda code write to this queue?" It turned out an S3 bucket was supposed to drop an event notification into the same queue whenever a new claim photo landed, and that part had never worked either — a second, separate Access Denied hiding behind the first one.
This is a different rule than the Lambda-role case. When the caller is an AWS service — not an IAM user or role — the SQS access policy must explicitly allow that service's principal. There's no IAM role behind an S3 bucket or an SNS topic to attach a policy to; the only place permission can live is the queue's own access policy.
S3 event notifications to SQS
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3EventNotifications",
"Effect": "Allow",
"Principal": { "Service": "s3.amazonaws.com" },
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:us-east-1:111122223333:claims-queue",
"Condition": {
"ArnLike": { "aws:SourceArn": "arn:aws:s3:::jakes-claim-photos" },
"StringEquals": { "aws:SourceAccount": "111122223333" }
}
}
]
}
The two condition keys matter more than they look. aws:SourceArn pins the statement to messages coming from that exact S3 bucket, not just "any request that claims to be S3." aws:SourceAccount adds a second check against the account that owns the bucket. Without both, you're trusting the Service principal alone, which any S3 bucket in any account could theoretically claim.
SNS topic fanning out to SQS, and EventBridge rules
Same shape, different Service value: sns.amazonaws.com for an SNS subscription delivering to the queue, or events.amazonaws.com for an EventBridge rule targeting it, each with an aws:SourceArn condition scoped to the specific topic or rule ARN. If you're troubleshooting a "messages never arrive" symptom rather than an outright error — SNS-to-SQS failures often fail silently instead of throwing — check the queue's access policy for a matching statement before assuming the subscription itself is broken.
Encryption adds a second gate: KMS permissions
If the queue has server-side encryption turned on with a customer managed AWS Key Management Service (KMS) key — a key you created yourself, as opposed to the AWS-managed default — fixing the SQS-level policy isn't the end of the story. KMS is a separate service with its own permission check, and it runs in addition to, not instead of, everything above.
🕐 What changed on the send side
- Before: producers writing to an SSE-enabled queue needed both
kms:Decryptandkms:GenerateDataKeyon the key. - Now: Amazon SQS no longer requires
kms:Decryptfor theSendMessageAPI — producers only needkms:GenerateDataKeyon the key used to encrypt the queue. - What that means for the steps above:
ReceiveMessage— the consumer side, which is what your Lambda function is usually doing — still needskms:Decrypton that key. Trim send-side KMS grants to justGenerateDataKey; don't carry the oldDecryptrequirement over to producers by habit.
Check whether a queue is encrypted with a customer managed key from the SQS console's queue details page, under Encryption, or by calling GetQueueAttributes and looking at the KmsMasterKeyId attribute. If it points at a key you own (rather than the AWS-managed alias/aws/sqs), your Lambda execution role needs a statement like this attached, and the key's own key policy needs to allow the role too:
{
"Effect": "Allow",
"Action": ["kms:Decrypt"],
"Resource": "arn:aws:kms:us-east-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
}
🙋♂️ Jake's Reality Check
"I picked the AWS managed key when I set up the queue because it seemed simpler. Am I stuck with it?"
Yes, in one important way — you can't edit an AWS-managed key's key policy. If you ever need cross-account access or a service other than SQS to use that key, you'll have to switch the queue to a customer managed key, because only those let you edit the policy that controls who can use them.
VPC endpoints: the silent third policy
If your Lambda function runs inside a VPC and reaches SQS through an interface VPC endpoint instead of the public internet — a common setup for functions that also need private access to a database — there's a third policy in play: the VPC endpoint policy itself. Even if the IAM role and the queue policy both say yes, a restrictive VPC endpoint policy that doesn't name your principal or your queue will still produce Access Denied, because the request never gets past the endpoint.
A VPC endpoint policy looks like this — narrowly scoped to one principal, one action, and one queue:
{
"Statement": [{
"Action": ["sqs:SendMessage"],
"Effect": "Allow",
"Resource": "arn:aws:sqs:us-east-2:123456789012:MyQueue",
"Principal": { "AWS": "arn:aws:iam:123456789012:role/MyLambdaRole" }
}]
}
If you don't know whether your function even uses a VPC endpoint for SQS, check the VPC console's Endpoints page for one whose service name ends in .sqs, and open its policy tab. Most accounts never create one, in which case this section simply doesn't apply — but if yours does, it's a common place to lose an afternoon, because the error message doesn't distinguish "the endpoint policy blocked this" from "the queue policy blocked this." Both come back as the same generic Access Denied.
It's worth being clear about what a VPC endpoint policy is not, too. It's not a security group, and it's not the same thing as a NAT gateway route. A security group controls network-level traffic in and out of the Lambda function's elastic network interfaces; a VPC endpoint policy controls which IAM principals and actions are allowed to use that specific private connection to SQS at the API level, the same way the queue policy controls it at the queue level. A function can have a perfectly open security group, a working private DNS entry for SQS, and still get Access Denied purely because the endpoint policy hasn't been told about the role making the call.
It also helps to separate two symptoms that get mixed together. A Lambda function inside a VPC with no route to the internet at all — no NAT gateway and no VPC endpoint for SQS — doesn't get Access Denied. It times out, because the request never reaches Amazon SQS in the first place. A VPC endpoint policy that's simply too narrow behaves differently: the request does reach SQS through the endpoint and comes back with the same AccessDenied error you'd see from a queue-policy problem. If your function times out instead of failing fast with a denial, the fix is routing — add a NAT gateway or a VPC endpoint — not a policy edit. If it fails fast with AccessDenied and a VPC endpoint is already configured, the endpoint's policy is the next thing to open, right alongside the queue policy.
The explicit Deny trap — and how to recover from it
An explicit "Effect": "Deny" statement, in either the IAM policy or the queue policy, overrides every Allow anywhere else — including an Allow in the other policy, an Allow granted through the root user, and an Allow you're absolutely certain you wrote correctly. This is the single most common cause of an Access Denied error that survives a policy rewrite: the fix keeps landing in the Allow statements while a Deny statement, often written for an unrelated reason months earlier, quietly wins every time.
| Error message you see | What it actually means |
|---|---|
AccessDenied: "Access to the resource ... is denied" | Generic SQS/IAM policy denial — check the account boundary, then both policies for a missing Allow or a present Deny. |
KMS.AccessDeniedException | The SQS-level policies are fine; the caller is missing kms:Decrypt or kms:GenerateDataKey on the queue's encryption key. |
| Function invokes but the queue never shows fewer messages | Often a silent VPC endpoint policy block, or a Deny statement scoped narrowly enough that other actions still work. |
If the Deny is broad enough — a wildcard Principal with no exceptions — you can end up locked out of the queue you own. Amazon SQS has a specific, documented recovery path for exactly this: use the queue-owner account's root user, which retains the ability to remove a Deny policy, or use the AWS-managed SQSUnlockQueuePolicy policy, added specifically to unlock a queue and remove a misconfigured policy that denies all principals.
⚠️ What this actually breaks
Never explicitly deny the root user account access to a queue policy. If you do, and the queue also has no other Allow statement carved out, there may be no path back in without opening a support case. Always pair a broad Deny with a narrower Allow statement for at least one entity you control, so someone can still get in to fix it.
AWS Organizations: the policy layer people forget entirely
If your AWS account belongs to an AWS Organization, there's a fourth possible source of Access Denied: an organization-wide service control policy (SCP). By default, AWS Organizations policies don't block any requests to Amazon SQS — this isn't something that trips people up out of the box. But if your organization's security team has written an SCP restricting service usage, region access, or specific actions, it applies on top of everything else in this post, and no queue policy or IAM policy edit will override it. If you've checked every policy above and you're still stuck, especially in a company AWS account rather than a personal one, ask whoever manages your Organization to list the policies attached to your account's organizational unit.
The telltale sign that you're looking at an SCP rather than an IAM or queue policy problem is scale: the same AccessDenied shows up across multiple unrelated resources and services in the account, not just this one queue, and it shows up for IAM users and roles that would otherwise clearly have permission. A single Lambda role failing against a single queue, with every other SQS action working fine elsewhere in the account, points back to the queue policy — not to Organizations.
- Ask whether the account sits in an AWS Organization at all. A personal or standalone AWS account has no SCPs to check, and you can skip this section entirely.
- If it does, list the SCPs attached to the account and its parent organizational unit. Someone with Organizations access can do this from the management account; you may need to ask a platform or security team if you don't have that access yourself.
- Look specifically for a
Denystatement referencingsqs:*, a specific Region, or a condition onaws:RequestedRegion. These are the three most common shapes an SQS-blocking SCP takes.
The full debugging checklist, in order
"Work top to bottom," Ethan told him, "and stop at the first thing that's actually wrong. Don't rewrite five policies at once — you'll fix it, sure, but you'll never know which change actually did it, and you'll be back here next month making the same guesses."
- Identify the exact caller. Get the Lambda execution role's ARN from the function's Configuration tab, and confirm which AWS account it belongs to.
- Confirm same-account vs. cross-account vs. service principal. Compare the role's account ID against the queue ARN's account ID; separately, check whether an AWS service (not your Lambda function) is the one actually failing.
- Read the queue's access policy as written, not as you remember writing it. Open the SQS console's Access policy tab in JSON view and look for both Allow and Deny statements, and any
Conditionblocks that might be narrower than they look. - Use the IAM Policy Simulator against the Lambda execution role to test the exact action and resource ARN that's failing — this tells you definitively whether the IAM side would allow it, in isolation from the queue policy.
- Check for a customer managed KMS key on the queue, and confirm both the IAM policy and the key's own key policy grant the right KMS actions.
- Check for a VPC endpoint if the function runs inside a VPC, and review its policy the same way you reviewed the queue policy.
- Check for an AWS Organizations SCP if the account is part of an organization, especially in a workplace or client account you don't fully control.
- Fix exactly one layer, then re-test the specific failing action — not a broader smoke test — before moving to the next layer.
Confirming the exact denial with CloudTrail
Everything above is about reasoning through which policy is likely responsible. There's also a way to confirm it directly. Amazon SQS added CloudTrail integration covering all of its APIs, which means every SendMessage, ReceiveMessage, DeleteMessage, or GetQueueAttributes call your Lambda function makes — allowed or denied — is recorded as an event you can search.
🙋♂️ Jake's Reality Check
"I don't even know where to look for that. Is it another console tab I've never opened?"
It's the CloudTrail console's Event history page. Filter by event name (SendMessage, ReceiveMessage) or by the Lambda execution role's ARN as the user name, in roughly the time window when the function ran. Open the matching event and look at the response elements — a denied call records the error code there, which tells you definitively whether you're looking at a plain AccessDenied, a KMS.AccessDeniedException, or something else entirely, instead of guessing from the exception text your function logged.
This matters most when your own reasoning and the error message seem to disagree — for example, when you're confident the queue policy is correct but the function still fails. CloudTrail won't tell you which line of JSON is wrong, but it will tell you conclusively which service issued the denial, which narrows four possible culprits (IAM, queue policy, KMS, VPC endpoint) down to one before you touch anything.
What AWSLambdaSQSQueueExecutionRole actually grants
Attaching a managed policy by name is easy; knowing what's actually inside it is what keeps you from over- or under-granting access. AWSLambdaSQSQueueExecutionRole — an AWS managed policy you can attach directly to the execution role — grants receive-message, delete-message, and read-attribute access to SQS queues, along with write permissions to CloudWatch Logs. In plain terms: it lets a function poll a queue, remove messages it's finished with, check queue attributes like approximate message count, and log what happened. It does not grant SendMessage, so a function that both consumes from one queue and publishes to another still needs a separate, explicit statement for the send side.
✅ Why this is the one to use
If your Lambda function's only job is to be triggered by an SQS event source mapping and process what arrives, this managed policy is the right default — it's scoped to exactly the receive/delete/read pattern that consumption requires, without the broader write and management actions a wildcard SQS policy would carry.
Tightening the queue policy without breaking access
Once access works, the next honest question is whether it's scoped too loosely. A queue policy that allows the account root with "Action": "SQS:*" is common, and it isn't automatically wrong — but if you're isolating a queue so that only a specific Lambda role (and, say, requests through a specific VPC endpoint) can send or receive, you want an Allow statement for the owner plus a narrower Deny for everyone else, built on condition keys like aws:PrincipalArn and aws:SourceVpce.
Jake looked at the wildcard statement they'd been using to get things working and asked whether he should just leave it alone now that it worked. Ethan didn't hesitate. "It's the laziest option on the list, and it's the one I like least," he said. "You've got exactly one Lambda role that's ever supposed to touch this queue. Write the policy for that one role — not for 'whatever shows up claiming to be it.'" Jake pushed back: wasn't that extra work for a queue nobody outside his own contractor even knew existed? "Nobody knows about it until someone does," Ethan said. "A leaked access key, a second account somebody forgot to lock down, a role assumed from somewhere it shouldn't be. A scoped-down policy is boring right up until the day it's the only thing standing between a mistake and a mess."
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "OwnerFullAccess",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:root" },
"Action": "SQS:*",
"Resource": "arn:aws:sqs:us-east-1:111122223333:claims-queue"
},
{
"Sid": "RestrictToLambdaRole",
"Effect": "Deny",
"Principal": "*",
"Action": ["sqs:SendMessage", "sqs:ReceiveMessage", "sqs:DeleteMessage"],
"Resource": "arn:aws:sqs:us-east-1:111122223333:claims-queue",
"Condition": {
"ArnNotEquals": { "aws:PrincipalArn": "arn:aws:iam::111122223333:role/claims-lambda-role" }
}
}
]
}
Test any Deny statement you add carefully with the Policy Simulator before relying on it in production. When a Condition block combines more than one condition key, review exactly how they're evaluated together for the specific operators you used — the evaluation logic for multiple keys and values is documented, but it's easy to assume it works like plain English "and" when the actual behavior needs a closer read. A restrictive-looking Deny that doesn't evaluate the way you expected is worse than no Deny at all, because it gives false confidence.
Dead-letter queues: a second access policy hiding in plain sight
A dead-letter queue, or DLQ, is a separate SQS queue that catches messages your Lambda function failed to process after a set number of tries — think of it as the returns bin at the back of Jake's shop instead of the shelf out front, so a handful of bad claim submissions don't jam up everything behind them. Most Access Denied troubleshooting stops at the main queue's policy and never looks at the DLQ's, which is exactly where a second, unrelated permission problem likes to hide.
Every SQS queue has its own redrive allow policy, a setting separate from the access policy covered everywhere else in this post. It controls which other queues in the account are allowed to name this queue as their dead-letter queue in the first place — not who can send or receive messages, but who can even pair with it.
| Redrive permission setting | What it allows |
|---|---|
| Allow all | Any queue in this AWS account, in the same Region, can specify this queue as its dead-letter queue. This is the default when no redrive allow policy is set. |
| By queue | Only the specific source queues you list, by ARN, can pair with this one as their DLQ — up to 10 source queues before you have to switch to Allow all. |
| Deny all | No queue can specify this one as a dead-letter queue, even if its redrive policy points here. |
If someone tightened a shared DLQ to "By queue" for security reasons and forgot to add your new queue's ARN to the list, messages that exceed your maxReceiveCount simply won't move — and depending on how you're watching for it, that can look identical to a broader access problem, even though the ordinary send/receive permissions on both queues are completely fine.
Redriving messages back out of the DLQ needs its own permissions
Once a batch of failed claim messages lands in the DLQ, moving them back to be retried — either through the SQS console's Start DLQ redrive button or the StartMessageMoveTask API action — is a completely separate permission set from anything a Lambda consumer needs day to day. To start a redrive, the caller needs sqs:StartMessageMoveTask, sqs:ReceiveMessage, sqs:DeleteMessage, and sqs:GetQueueAttributes on the dead-letter queue itself, plus sqs:SendMessage on whichever queue the messages are moving to. If either queue is encrypted with a KMS key, add the matching kms:Decrypt or kms:GenerateDataKey permissions on that key as well.
⚠️ What this actually breaks
Popular advice often says "just give the person doing the redrive full SQS access on both queues." That works, but it's wider than the task needs — a redrive only ever needs the four DLQ-side actions and one send action on the destination, never PurgeQueue, SetQueueAttributes, or access to any other queue in the account.
CloudFormation and Terraform: where this usually goes wrong
If you're managing the queue and the Lambda function through infrastructure as code, the failure pattern is usually the same: the execution role's IAM policy gets defined in the template alongside the function, and the queue policy gets forgotten because it "belongs" to a different resource block, sometimes in a different stack or a different repository entirely, especially in the cross-account case.
A few things worth checking specifically:
- Circular dependency between the role ARN and the queue policy. The queue policy needs the exact execution role ARN, but if the role is created in the same stack as a function that also needs the queue's ARN, you can end up needing both values before either resource exists. Split these into two applies, or pass the role ARN as a parameter into the stack that owns the queue.
- Stack updates that silently overwrite the queue policy. If a template manages the queue's
AccessPolicyDocumentas a full replacement rather than an additive change, redeploying the queue's own stack can wipe out a statement that was added by hand in the console — always keep the full policy in code once you've hand-edited a fix, so it doesn't disappear on the next deploy. - Default IAM policies from scaffolding tools (the AWS Serverless Application Model, the Serverless Framework, AWS CDK) often auto-generate a reasonably scoped IAM policy for the execution role, but none of them auto-generate the cross-account queue-policy statement, because they can't know about a queue in an account they don't manage.
In CloudFormation, the queue policy is its own resource type, separate from the queue itself — it's easy to define the queue and forget to also define this:
ClaimsQueuePolicy:
Type: AWS::SQS::QueuePolicy
Properties:
Queues:
- !Ref ClaimsQueue
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: AllowCrossAccountLambdaRole
Effect: Allow
Principal:
AWS: "arn:aws:iam::111122223333:role/cross-account-lambda-sqs-role"
Action:
- "sqs:ReceiveMessage"
- "sqs:DeleteMessage"
- "sqs:GetQueueAttributes"
Resource: !GetAtt ClaimsQueue.Arn
Terraform's equivalent is the aws_sqs_queue_policy resource, and it's just as easy to define the aws_sqs_queue without ever writing its policy counterpart:
resource "aws_sqs_queue_policy" "claims_queue" {
queue_url = aws_sqs_queue.claims_queue.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "AllowCrossAccountLambdaRole"
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::111122223333:role/cross-account-lambda-sqs-role" }
Action = ["sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"]
Resource = aws_sqs_queue.claims_queue.arn
}]
})
}
In both tools, if the queue and the policy live in separate state files or separate stacks owned by different teams — common in the exact cross-account setup this post is about — a change to one won't automatically trigger a review of the other. Treat the queue policy as a first-class resource in your infrastructure code, not an afterthought bolted onto the queue.
Edge cases that don't fit the standard fix
FIFO queues and cross-account access
Everything in this post applies the same way to FIFO queues (queue names ending in .fifo) as to standard queues — the access-policy mechanics don't change based on queue type. What does change is that FIFO queue names and the message group ID logic are separate concerns from access control; don't conflate a message-ordering issue with a permissions one just because both showed up the same afternoon.
Multiple queues, one Lambda function
Amazon SQS documentation is specific about the relationship here: you can configure multiple queues as event sources for a single Lambda function, but a given SQS queue can only be mapped to a single Lambda function at a time. If you're trying to fan the same queue out to two functions, that mapping itself — not a policy — is the blocker; you'll need a second queue or an SNS topic in front of both.
Someone else's break-glass access
If a security or platform team needs emergency access to a queue that's normally locked down to one Lambda role, don't weaken the standing policy to accommodate a one-time need. Grant temporary access through a short-lived assumed role or a time-boxed IAM policy instead, and remove it afterward. A queue policy edited under time pressure and never revisited is how "temporary" access from six months ago quietly becomes permanent.
Frequently asked questions
Why does my Lambda function get Access Denied on SQS even though the execution role has full SQS permissions?
Almost always because the queue is in a different AWS account than the function. In that case, an Allow on the IAM role alone produces an implicit deny — the queue's own access policy must separately name the role's ARN. Check the account ID in the queue's ARN against the role's ARN first.
Do I need to add a queue policy for Lambda in the same AWS account?
No, not for the standard case. In the same account, either the IAM policy or the queue policy granting Allow is enough on its own. Most same-account setups only need the execution role's IAM policy edited.
What's the difference between the SQS queue policy and the Lambda execution role's IAM policy?
The IAM policy is attached to the Lambda execution role (an identity) and controls what that role can do across AWS. The queue policy is attached to the SQS queue itself (a resource) and controls who's allowed to act on that specific queue, regardless of what their own IAM policy says.
My queue is encrypted with a KMS key — what extra permission do I need?
Your Lambda execution role needs kms:Decrypt on that key to call ReceiveMessage. If your function also sends messages to an encrypted queue, it needs kms:GenerateDataKey for the send side — kms:Decrypt is no longer required for SendMessage.
How do I grant a Lambda function in Account A access to an SQS queue in Account B?
Account B's queue owner adds a statement to the queue's access policy naming Account A's Lambda execution role ARN as the Principal, scoped to the SQS actions needed. Account A then creates the event source mapping via the CLI or an infrastructure tool, pointing at the queue's full ARN in Account B, in the same Region.
What IAM actions does the AWSLambdaSQSQueueExecutionRole managed policy include?
It grants sqs:ReceiveMessage, sqs:DeleteMessage, and read access to queue attributes, plus permission to write logs to CloudWatch. It does not include sqs:SendMessage — add that separately if your function also publishes messages.
Why is my S3 bucket getting Access Denied when sending event notifications to SQS?
Because S3 is calling SQS as a service principal, not through an IAM role, so the queue's access policy needs a statement allowing s3.amazonaws.com as the Principal, scoped with aws:SourceArn to your specific bucket and aws:SourceAccount to your account ID.
Can I lock myself out of my own SQS queue with a bad policy?
Yes. A broad explicit Deny with no carved-out exception can block every principal, including the queue owner's own account. This is a real, documented failure mode, not a hypothetical one.
What is the SQSUnlockQueuePolicy and when do I use it?
It's an AWS managed policy specifically built to unlock a queue and remove a misconfigured policy that's denying all principals. Use it, or root-user access, when a Deny statement has locked out your normal IAM users and roles entirely.
Does an explicit Deny in either policy always win?
Yes. An explicit Deny in the IAM policy or the queue policy overrides any Allow anywhere else, including an Allow in the other policy.
My Lambda function is in a VPC — do I need a VPC endpoint policy too?
Only if your function reaches SQS through an interface VPC endpoint rather than the public internet. If you have one configured, its policy must also allow your principal and action, separate from the queue's own access policy.
Why do I get Access Denied only on ReceiveMessage but SendMessage works fine?
Check your KMS permissions first if the queue is encrypted with a customer managed key — ReceiveMessage still requires kms:Decrypt, while SendMessage no longer does. If the queue isn't encrypted, compare the specific actions listed in your IAM and queue policies; a policy scoped to sqs:SendMessage alone won't cover receiving.
Can AWS Organizations block my Lambda function from reaching SQS?
By default, no — AWS Organizations policies don't block SQS requests out of the box. But if your organization has configured a service control policy that restricts SQS or the region you're operating in, it applies above every policy in this post and can't be overridden from inside the member account.
How do I test whether my IAM policy would actually allow the action, before deploying?
Use the IAM Policy Simulator against the Lambda execution role, entering the exact SQS action and the queue's ARN. It evaluates the same way AWS would at request time, without you having to redeploy the function to find out.
What's the exact SQS queue policy JSON I should use to allow one specific Lambda role?
An Allow statement with the Lambda execution role's full ARN as the Principal, the specific SQS actions the function needs as the Action array, and the queue's ARN as the Resource — see the cross-account policy example earlier in this post for the exact structure.
Do I need sqs:GetQueueAttributes and sqs:GetQueueUrl too, or just SendMessage/ReceiveMessage?
For a standard event-source-mapping consumer, include sqs:GetQueueAttributes alongside ReceiveMessage and DeleteMessage — Lambda's polling mechanism reads queue attributes as part of normal operation. sqs:GetQueueUrl only matters if your own code looks up the queue by name at runtime rather than using a hardcoded ARN or URL.
Jake's fix, in the end, took fifteen minutes once he knew where to look: one statement added to the contractor's queue policy naming his execution role's ARN, and a second statement letting S3 write to the same queue with the source bucket locked down by ARN. The Lambda role he'd already set up with "full access" had been correct the entire time — it just never had a chance to matter until the queue's own policy agreed to let it in.
Revision note. Written September 2026, covering current Amazon SQS and AWS Lambda access-policy behavior, including the SQSUnlockQueuePolicy managed policy and the current KMS permission split between SendMessage and ReceiveMessage. AWS updates IAM condition keys and managed policy contents from time to time, so if a console menu doesn't match what's described here, the underlying account-boundary logic in this post will still hold. If you're staring at a wall of red AccessDenied text right now, take a breath — this is one of the most fixable errors in all of AWS, and you're closer to done than it feels.