SQS IAM least privilege is a lot more nuanced than just granting sqs:ReceiveMessage and sqs:DeleteMessage.

Let’s see it in action with a typical consumer setup. Imagine a Python application using boto3 to poll an SQS queue.

import boto3
import json
import os

sqs = boto3.client('sqs')
queue_url = os.environ['QUEUE_URL']
visibility_timeout = int(os.environ.get('VISIBILITY_TIMEOUT', 30))

def process_message(message_body):
    print(f"Processing: {message_body}")
    # Simulate work
    pass

while True:
    try:
        response = sqs.receive_message(
            QueueUrl=queue_url,
            MaxNumberOfMessages=1,
            VisibilityTimeout=visibility_timeout,
            WaitTimeSeconds=20
        )

        if 'Messages' in response:
            message = response['Messages'][0]
            receipt_handle = message['ReceiptHandle']
            message_body = message['Body']

            try:
                process_message(message_body)
                sqs.delete_message(
                    QueueUrl=queue_url,
                    ReceiptHandle=receipt_handle
                )
                print(f"Deleted message with receipt handle: {receipt_handle}")
            except Exception as e:
                print(f"Error processing message: {e}")
                # In a real app, you might want to move to a Dead Letter Queue
                # or handle retries differently. For simplicity, we'll let it
                # become visible again after visibility_timeout.
        else:
            print("No messages received.")

    except Exception as e:
        print(f"Error receiving message: {e}")
        # Handle network issues, etc.
        import time
        time.sleep(5)

This code snippet highlights the core operations: receive_message, delete_message. But true least privilege goes deeper. The goal is to grant only the permissions necessary for a specific consumer role to perform its job, and nothing more. This minimizes the blast radius if that role’s credentials are compromised.

The fundamental problem SQS IAM least privilege solves is preventing unauthorized access to messages or the ability to manipulate queues. Without fine-grained permissions, a compromised consumer could:

  • Read sensitive data from queues it shouldn’t access.
  • Delete messages from other queues, causing data loss.
  • Change queue attributes like VisibilityTimeout or MessageRetentionPeriod, impacting delivery and data availability.
  • Send messages to queues it’s only supposed to consume from.

A typical IAM policy for a consumer might look like this:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sqs:ReceiveMessage",
                "sqs:DeleteMessage",
                "sqs:GetQueueAttributes"
            ],
            "Resource": "arn:aws:sqs:us-east-1:123456789012:my-consumer-queue"
        }
    ]
}

This is a good start, but it’s not granular enough for true least privilege. For instance, sqs:GetQueueAttributes allows reading all queue attributes, not just the ones needed for receiving messages.

The most crucial lever you control is the Resource element in your IAM policy. Instead of granting permissions to a wildcard (*) or even just the queue ARN, you can restrict it further.

Consider a scenario where your consumer only needs to receive messages and delete them after successful processing. It absolutely does not need to send messages or change queue configurations.

Here’s a more restrictive policy for a consumer role:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "sqs:ReceiveMessage",
                "sqs:DeleteMessage"
            ],
            "Resource": "arn:aws:sqs:us-east-1:123456789012:my-consumer-queue"
        },
        {
            "Effect": "Allow",
            "Action": "sqs:GetQueueAttributes",
            "Resource": "arn:aws:sqs:us-east-1:123456789012:my-consumer-queue",
            "Condition": {
                "StringEquals": {
                    "sqs:QueueAttributeName": [
                        "VisibilityTimeout",
                        "ApproximateNumberOfMessages"
                    ]
                }
            }
        }
    ]
}

The key here is the Condition block on sqs:GetQueueAttributes. By specifying sqs:QueueAttributeName and listing only VisibilityTimeout and ApproximateNumberOfMessages, you prevent the consumer from fetching other, potentially sensitive or configuration-related, attributes like QueueArn, MessageRetentionPeriod, or RedrivePolicy. This is a powerful way to limit the information a compromised role can exfiltrate or use to infer system design.

Furthermore, if you have multiple SQS queues, never use a wildcard (*) for the Resource in your consumer’s IAM policy. Explicitly list each queue ARN or use a naming convention that allows for ARNs to be specified programmatically if you have many queues. For example, if your queues follow a pattern like my-app-worker-queue-*, you might use arn:aws:sqs:us-east-1:123456789012:my-app-worker-queue-*. However, even this is less secure than explicit ARNs if possible.

The sqs:ReceiveMessage action implicitly grants the ability to see message attributes, which can sometimes contain sensitive information. If your consumer truly doesn’t need to inspect message attributes at all, you can enforce that using a condition on sqs:ReceiveMessage itself, though this is rarely practical as most consumers need at least some message metadata. The sqs:ReceiveMessage action has a aws:RequestTag condition key that can be used to restrict based on tags, but this isn’t directly applicable to message attributes. The closest you can get for message attributes is often handled at the sender side by not putting sensitive data there, or by encrypting the message body itself.

One thing most people don’t know is how sqs:ReceiveMessage interacts with VisibilityTimeout. When a consumer receives a message, it’s not deleted. Instead, its visibility is turned off for a period defined by VisibilityTimeout. If the consumer successfully processes the message within this timeout, it calls sqs.delete_message using the ReceiptHandle. If it fails to delete the message before the timeout expires, the message becomes visible again in the queue, and another consumer can pick it up. This mechanism is critical for ensuring message processing even if a consumer crashes, but it also means that a malicious consumer could intentionally let messages time out without deleting them, effectively creating a denial-of-service by making messages unavailable to other legitimate consumers. A least-privilege policy cannot directly prevent this specific behavior (making messages reappear), but it can limit the damage by ensuring the compromised consumer can only affect its designated queue.

The next concept you’ll likely grapple with is how to manage these fine-grained permissions effectively as your application scales and your SQS topology grows.

Want structured learning?

Take the full Sqs course →