Mastering Cross-Account SNS to SQS Subscriptions with AWS CDK

In a modern microservices architecture, decoupled communication is king. AWS SNS (Simple Notification Service) and SQS (Simple Queue Service) are the bedrock of this decoupling. But as your organization grows and you move toward a multi-account strategy, you'll inevitably hit a common hurdle: How do you subscribe an SQS queue in Account B to an SNS topic in Account A?
Cross-account subscriptions can be tricky because of permission boundaries and the "Pending Confirmation" dance. In this post, we’ll explore two ways to handle this using AWS CDK, and why one is clearly superior.
The Scenario
Imagine a Core Service in Account A that publishes "Task Status" events to an SNS Topic. A Consumer Service in Account B needs to process these events via an SQS Queue.
Important: For native SNS-to-SQS subscriptions, make sure the Topic and Queue are in the same AWS Region.
Method 1: Subscribe as the Queue Owner (Recommended)
In this approach, the owner of the SNS Topic (Account A) "opens the door" by granting permission, and the owner of the SQS Queue (Account B) "walks in" by creating the subscription.
Granting Consumer Account the Permission to Subscribe
Instead of managing every single subscription, the topic owner grants sns:Subscribe permissions to the consumer account.
CDK Implementation (Account A):
const topic = new sns.Topic(this, "StatusTopic", {
fifo: true,
contentBasedDeduplication: true,
});
// Grant a specific account permission to subscribe
topic.grantSubscribe(new iam.AccountPrincipal("123456789012"));
// OR: If you are in an AWS Organization, grant access to the entire Org
topic.addToResourcePolicy(new iam.PolicyStatement({
actions: ["sns:Subscribe"],
resources: [topic.topicArn],
principals: [new iam.AnyPrincipal()],
conditions: {
StringEquals: { "aws:PrincipalOrgID": "o-xxxxxxxxxx" }
}
}));
Subscribing SQS Queue to the Topic
Now, the consumer can subscribe their queue to the topic using the Topic ARN. Since they have permission on the topic and own the queue, the subscription is confirmed instantly.
CDK Implementation (Account B):
const queue = new sqs.Queue(this, "ConsumerQueue", { fifo: true });
const topicArn = "arn:aws:sns:us-east-1:111122223333:StatusTopic.fifo";
const topic = sns.Topic.fromTopicArn(this, "ImportedTopic", topicArn);
topic.addSubscription(new subs.SqsSubscription(queue, {
rawMessageDelivery: true,
filterPolicy: {
status: sns.SubscriptionFilter.stringFilter({ allowlist: ["CREATED"] }),
},
}));
Deployment tip: Deploy Account A (Topic policy/grants) before Account B creates the subscription.
The "Self-Service" Advantage
This is the Gold Standard for microservices. Account A provides the "Event Bus" (the Topic), and consumers can:
Create as many queues as they need.
Define their own Filter Policies without bothering the producer.
Manage their own DLQs and retry logic.
Scale independently without any manual "confirmation" steps.
Method 2: Subscribe as the Topic Owner
Here, the producer in Account A explicitly creates the subscription to the remote queue in Account B.
Creating a Subscription for the Remote SQS Queue
Instead of allowing the consumers to initiate the subscriptions, the topic owner creates and manages the subscriptions for the consumers.
CDK Implementation (Account A):
const queue = sqs.Queue.fromQueueArn(this, "RemoteQueue", "arn:aws:sqs:us-east-1:444455556666:ConsumerQueue.fifo");
topic.addSubscription(new subs.SqsSubscription(queue));
The "Symmetric Permission" Requirement
For this to work, you must also update the SQS Queue Policy in Account B to allow the SNS Topic to send messages.
CDK Implementation (Account B):
// In Account B's CDK code:
queue.addToResourcePolicy(new iam.PolicyStatement({
actions: ["sqs:SendMessage"],
resources: [queue.queueArn],
principals: [new iam.ServicePrincipal("sns.amazonaws.com")],
conditions: {
ArnEquals: { "aws:SourceArn": "arn:aws:sns:us-east-1:111122223333:StatusTopic.fifo" },
StringEquals: { "aws:SourceAccount": "111122223333" }
}
}));
The Catch: When the producer creates the subscription cross-account, it enters a Pending Confirmation state. AWS sends a SubscriptionConfirmation message to the SQS queue. The consumer then needs a confirmation step (manual or automated, e.g., a Lambda that calls ConfirmSubscription). This introduces extra coupling and operational complexity versus Method 1.
Subscription confirmation
Step 1: In Account B, search for the subscription confirmation message by polling the queue messages.
Step 2: Extract the
SubscribeURLfrom the message and enter it in a browser to confirm.
Step 3: Now in Account A, verify that the subscription is confirmed.
Essential Best Practices
1. Use Filter Policies
Don't make your consumers process every single message. Use SubscriptionFilter to ensure they only get what they need.
topic.addSubscription(new subs.SqsSubscription(queue, {
filterPolicy: {
status: sns.SubscriptionFilter.stringFilter({
allowlist: ["CREATED", "COMPLETED"],
}),
},
}));
2. Dead-Letter Queues (DLQs)
Always attach a DLQ to your SQS queue. If a message fails to process after several retries, it moves to the DLQ instead of blocking the queue.
const dlq = new sqs.Queue(this, "DeadLetterQueue", {
fifo: true,
retentionPeriod: cdk.Duration.days(14),
});
const queue = new sqs.Queue(this, "ConsumerQueue", {
fifo: true,
deadLetterQueue: {
queue: dlq,
maxReceiveCount: 5, // Move to DLQ after 5 failed attempts
},
});
3. Encryption with KMS
When using Customer Managed Keys (CMKs) across accounts, KMS permissions depend on the exact service interactions:
SNS-Encrypted Topics: Your Publishers (IAM roles or AWS services) must have
kms:GenerateDataKey*andkms:Decrypton the Topic's key to encrypt payloads. Thesns.amazonaws.comprincipal does not need permissions on this key to fan out messages.SQS-Encrypted Queues: Because SNS writes to the queue, you must grant the
sns.amazonaws.comservice principalkms:GenerateDataKey*andkms:Decrypton the Queue's key. Downstream Consumers also requirekms:Decrypton this key to read the messages.Cross-Account Setup: Access requires two-way trust. You must explicitly allow the cross-account actions in both the KMS Key Policy (in the key-owning account) and the IAM Policy (in the calling account). If either is missing, access fails.
Grant Amazon SNS KMS permissions to Amazon SNS to publish messages to the queue
// Create the Customer Managed Key (CMK) for the SQS Queue
const queueKey = new kms.Key(this, "QueueKey", {
enableKeyRotation: true,
alias: "alias/my-queue-key",
});
// Allow SNS Service Principal to use the Queue's KMS Key
queueKey.addToResourcePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal("sns.amazonaws.com")],
actions: ["kms:GenerateDataKey*", "kms:Decrypt"],
resources: ["*"], // Evaluates to this specific key
conditions: {
// Security Best Practice: Restrict to the specific cross-account topic
ArnEquals: { "aws:SourceArn": crossAccountTopicArn }
}
}));
Restrict message transmission to a specific Amazon SNS topic
// Create the Encrypted SQS Queue
const queue = new Queue(this, "MyEncryptedQueue", {
encryption: sqs.QueueEncryption.KMS,
encryptionMasterKey: queueKey,
});
// Allow the cross-account SNS topic to publish messages to the Queue
queue.addToResourcePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal("sns.amazonaws.com")],
actions: ["sqs:SendMessage"],
resources: [queue.queueArn],
conditions: {
ArnEquals: { "aws:SourceArn": crossAccountTopicArn }
}
}));
Tip: The
ArnEquals: { 'aws:SourceArn': crossAccountTopicArn }condition is crucial to prevent the confused deputy problem, ensuring only your specific topic can use this key and queue.
4. FIFO Symmetry
If you're using FIFO (as in the examples), ensure both the Topic and the Queue are FIFO. A FIFO Topic cannot deliver to a standard Queue, and a standard Topic cannot deliver to a FIFO Queue. Also, make sure your publishers provide a MessageGroupId.
Summary
Cross-account messaging is a powerful pattern for scaling. By granting subscription permissions at the Topic level (Method 1), you enable a self-service model that empowers consumers and keeps your infrastructure code clean and automated.




