Skip to content

feat(sns-subscriptions): support cross-region delivery from opt-in regions - #37890

Closed
DavidA94 wants to merge 2 commits into
aws:mainfrom
DavidA94:feat/sns-subscription-opt-in-regions
Closed

feat(sns-subscriptions): support cross-region delivery from opt-in regions#37890
DavidA94 wants to merge 2 commits into
aws:mainfrom
DavidA94:feat/sns-subscription-opt-in-regions

Conversation

@DavidA94

Copy link
Copy Markdown

Issue # (if applicable)

Closes #37873.

Reason for this change

Amazon SNS supports cross-region delivery to AWS Lambda functions and Amazon SQS queues, but when one of the regions is an opt-in region (ap-east-1, me-south-1, eu-south-1, af-south-1, il-central-1, etc.) the subscriber's resource policy must trust the regionalized SNS service principal sns.<region>.amazonaws.com instead of (or in addition to) the default sns.amazonaws.com. AWS documents this rule on the cross-region delivery page and the Lambda prerequisites page.

LambdaSubscription and SqsSubscription previously hardcoded sns.amazonaws.com, so customers could not express this scenario through the L2 API. The dead-letter queue resource policy created by the L2 Subscription class in aws-sns/lib/subscription.ts had the same gap. Workarounds (drop down to L1 CfnSubscription, or hand-attach an extra resource-policy statement after the fact) are unreasonable for a documented and supported AWS scenario.

Description of changes

Adds two optional, backward-compatible properties to both LambdaSubscriptionProps and SqsSubscriptionProps:

  • includeDefaultServicePrincipal (default true) — controls whether the default sns.amazonaws.com principal is granted permission. Set to false only when the topic is in an opt-in region and the subscriber should not also accept invocations from default-enabled regions.
  • additionalServicePrincipalRegions — opt-in regions whose regionalized SNS service principals should also be granted permission.

Both properties unset preserves today's behavior exactly. Existing CloudFormation logical IDs are preserved when the default principal is enabled, so existing stacks are not affected.

The shared logic (validation + principal construction) lives in a private helper snsServicePrincipals in aws-sns-subscriptions/lib/private/util.ts, consumed by both subscription types. Validation rules:

  • Tokenized region values are rejected (CloudFormation logical IDs cannot contain tokens).
  • Known opt-in regions (per aws-cdk-lib/region-info) are accepted.
  • Known default-enabled regions are rejected — the regionalized principal is a no-op for them and almost certainly indicates a user error.
  • Regions unknown to RegionInfo produce a synth-time warning so customers are not blocked when AWS launches a new opt-in region before region-info catches up.

Lambda emits one AWS::Lambda::Permission resource per principal, since that CFN resource type only accepts a single string in its Principal field. The original logical ID is preserved for the default-principal permission. SQS adds all principals to a single resource-policy statement (and to the KMS key policy when the queue is encrypted).

The same regional principals are also threaded through to the dead-letter queue resource policy. A new optional deadLetterQueueServicePrincipals prop on SubscriptionOptions (in aws-sns/lib/subscription.ts) lets the L2 Subscription class — directly, or via the helpers — configure which principals can write to the DLQ. When omitted, today's default of sns.amazonaws.com is preserved.

Alternatives considered

  • A single array prop with a 'default' magic string (e.g. principalRegions: ['default', 'ap-east-1']). Rejected: magic strings are inconsistent with the rest of aws-cdk-lib, and splitting into two props makes each prop's job unambiguous.
  • Putting the new props on the shared SubscriptionProps base interface in aws-sns-subscriptions/lib/subscription.ts. Rejected: it would expose the props on EmailSubscription, SmsSubscription, UrlSubscription, and FirehoseSubscription, where they would be silently ignored — a footgun. Direct addition to LambdaSubscriptionProps and SqsSubscriptionProps keeps the type system honest.
  • A region-format regex. An earlier draft included /^[a-z]{2,}-[a-z]+-\d+$/ as a pre-check. Rejected: the codebase has no precedent for regex-based region validation; the convention everywhere else (Stack.region, RegionInfo.regions, aws-lambda/lib/lambda-insights.ts) is to consult region-info's known-region list. Using Fact.find(region, FactName.IS_OPT_IN_REGION) also produces a more accurate error message for cases like us-gov-west-1 ("not an opt-in region" rather than the misleading "not a valid region identifier").
  • iam.CompositePrincipal for Lambda. Not viable — Lambda's addPermission requires a principal that exposes a string service (or accountId / arn) field, which CompositePrincipal does not have.
  • Auto-detection from topic.env.region and subscriber.env.region. Rejected — would silently misbehave for env-agnostic stacks (where topic.env.region is a Token) and for cross-account scenarios where the topic is imported by ARN. Explicit configuration matches CDK's convention of preferring user intent over inference for security-relevant policy.
  • Splitting the DLQ fix into a follow-up PR. Initially considered because the DLQ logic lives in a different module (aws-sns vs aws-sns-subscriptions), but the user-facing feature is one cohesive thing — without the DLQ wiring, the same configuration that works for the main subscription would silently fail for its DLQ. Shipping the feature half-complete is worse than the slightly bigger diff.

FirehoseSubscription is intentionally not changed: per the AWS cross-region delivery documentation, only Lambda and SQS support cross-region delivery. Firehose subscriptions use a customer-provided IAM role assumed via STS, so the regionalized service principal does not apply.

Describe any new or updated permissions being added

No new IAM permissions are required to use the feature. The change adjusts the principal side of existing permissions:

  • AWS::Lambda::Permission — emits an additional resource per opt-in region with Principal: sns.<region>.amazonaws.com (only when additionalServicePrincipalRegions is set).
  • AWS::SQS::QueuePolicy (subscription queue) — adds sns.<region>.amazonaws.com to the Service array of the existing statement (same condition: aws:SourceArn equals the topic ARN).
  • AWS::KMS::Key policy (when the SQS queue is encrypted) — adds sns.<region>.amazonaws.com to the Service array of the kms:Decrypt/kms:GenerateDataKey statement.
  • AWS::SQS::QueuePolicy (dead-letter queue) — adds sns.<region>.amazonaws.com to the Service array (same aws:SourceArn condition).

All additions are scoped by aws:SourceArn to the specific topic and only fire when the user opts in via additionalServicePrincipalRegions. There is no broadening of the default permission set.

Description of how you validated changes

Unit tests: added 14 new tests in packages/aws-cdk-lib/aws-sns-subscriptions/test/subs.test.ts covering Lambda + SQS happy paths, the includeDefaultServicePrincipal=false path, the DLQ path on both Lambda and SQS, the rejected-token path, the rejected default-enabled-region path, the rejected non-public-partition path, and the warning-on-unknown-region path. All 100 tests in aws-sns-subscriptions/test/ and aws-sns/test/subscription.test.ts pass, including the 86 pre-existing tests (so backwards compatibility is verified end-to-end).

Integration tests: added two IntegTest-based integ tests under packages/@aws-cdk-testing/framework-integ/test/aws-sns-subscriptions/test/:

  • integ.sns-lambda-opt-in-region.ts — topic in ap-east-1, Lambda + DLQ in us-east-2. Exercises the end-to-end wiring including the DLQ resource policy.
  • integ.sns-sqs-opt-in-region.lit.ts — topic in ap-east-1, queue in us-east-2. The lit form is referenced from the README example.

Snapshot generation note: the realistic scenario (topic in an opt-in region) cannot be deployed in CI without an AWS account that has opted into ap-east-1. Snapshots were generated with yarn integ --dry-run --update-on-failed, which is generally discouraged by CONTRIBUTING.md but follows the established precedent in integ.dynamodb-v2.cross-account-replica.ts, whose top-of-file comment explicitly instructs contributors to use --dry-run for non-deployable scenarios. A maintainer with an opt-in-region account can re-validate against a real deployment if desired; the integ test files include top-of-file comments documenting how to do so.

Build: clean yarn build for aws-cdk-lib and @aws-cdk-testing/framework-integ. npx lerna run build across all 61 jsii packages succeeds. jsii-rosetta extract --compile succeeds for all README snippets, including the two new examples.

Checklist


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

…gions

When an SNS topic and its Lambda or SQS subscriber straddle an
opt-in region (e.g. `ap-east-1`, `me-south-1`), AWS requires the
subscriber's resource policy to trust the regionalized SNS service
principal `sns.<region>.amazonaws.com` instead of (or in addition
to) the default `sns.amazonaws.com`. `LambdaSubscription` and
`SqsSubscription` previously hardcoded the default principal, so
this scenario could not be expressed through the L2 API.

Add two optional, backward-compatible properties to both
subscription types:

- `includeDefaultServicePrincipal` (default `true`) controls
  whether the default `sns.amazonaws.com` principal is granted
  permission.
- `additionalServicePrincipalRegions` lists opt-in regions whose
  regionalized SNS service principals should also be granted
  permission.

Both unset preserves today's behavior exactly. Synth-time
validation against `aws-cdk-lib/region-info` rejects known
default-enabled regions and tokenized values, and warns when a
region is not yet recognized so customers are not blocked when
AWS launches a new opt-in region.

Lambda emits one `Lambda::Permission` per principal (the CFN
resource type accepts only a single principal); the original
logical ID is preserved for the default-principal permission so
existing stacks are not affected. SQS adds all principals to a
single resource-policy statement.

The same regional principals are also threaded through to the
dead-letter queue resource policy. A new optional
`deadLetterQueueServicePrincipals` prop on `SubscriptionOptions`
lets callers (including the L2 `Subscription` class directly)
configure which principals can write to the DLQ; when omitted,
today's default of `sns.amazonaws.com` is preserved.

Integration test snapshots were generated with `--dry-run`
because the realistic scenario (topic in an opt-in region) cannot
be deployed without the AWS account having opted into that region.
This follows the precedent set by
`integ.dynamodb-v2.cross-account-replica.ts`, whose top-of-file
comment explicitly instructs contributors to use `--dry-run` for
non-deployable scenarios.

fixes aws#37873

Co-authored-by: Kiro 🤖 <noreply@kiro.dev>
@aws-cdk-automation aws-cdk-automation added the pr/needs-community-review This PR needs a review from a Trusted Community Member or Core Team Member. label Jul 28, 2026
@DavidA94

Copy link
Copy Markdown
Author

Closing in favor of #38572, which extends the auto-detecting snsServicePrincipal() helper introduced by #38339 to LambdaSubscription and to the dead-letter queue resource policy.

While this PR was awaiting review, #38339 landed with a different design for the same underlying problem: instead of adding explicit opt-in props (includeDefaultServicePrincipal, additionalServicePrincipalRegions), the merged solution auto-detects the correct principal from the topic and subscriber stack regions. That design covers the common case with zero user configuration, and the maintainers chose to ship it — a reasonable trade-off I want to respect.

However, #38339 only touched SqsSubscription. LambdaSubscription and the DLQ resource policy in aws-sns/lib/subscription.ts still hardcode sns.amazonaws.com, so the same silent-drop bug still fires for Lambda subscribers and for DLQs that cross an opt-in region. That's the residual scope, and #38572 is a small follow-up that fixes those two paths using the helper that #38339 already introduced. It also relocates the helper from aws-sns-subscriptions/lib/private/util.ts to aws-sns/lib/private/service-principal.ts so aws-sns/lib/subscription.ts can consume it without a circular import; util.ts re-exports the helper so #38339's sqs.ts import is unchanged.

Thanks to whoever eventually gets to review these — apologies for the wait on this one, and thanks to @xkjjx for driving the SQS fix in the meantime.

@DavidA94 DavidA94 closed this Aug 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Comments on closed issues and PRs are hard for our team to see.
If you need help, please open a new issue that references this one.

@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Aug 14, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

beginning-contributor [Pilot] contributed between 0-2 PRs to the CDK feature-request A feature should be added or improved. p2 pr/needs-community-review This PR needs a review from a Trusted Community Member or Core Team Member. pr/needs-further-review PR requires additional review from our team specialists due to the scope or complexity of changes.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

(sns-subscriptions): LambdaSubscription and SqsSubscription do not support regionalized service principals for cross-region delivery

3 participants