Back to blog
AWSBest PracticesCloud SecurityIdentity & AccessServerless

SNS Topic Allows Cross-Account Access: Risk and Remediation

Learn how SNS cross-account access leaks data and enables spoofing, plus step-by-step CLI and Terraform fixes to scope and lock down your SNS topic policies.

TL;DR

This check flags SNS topic policies that grant publish or subscribe access to a principal in a different AWS account. Cross-account access is sometimes intentional, but a misconfigured statement can let an outside account drain, spoof, or hijack your notifications. Scope every cross-account grant to a specific account ARN and condition, never a wildcard.

Amazon SNS sits in the plumbing of a lot of architectures. It fans out events to queues, triggers Lambdas, pages on-call engineers, and bridges accounts in larger organizations. Because it is so well connected, an SNS topic with a loose access policy becomes a quiet but useful foothold for an attacker. This Lensix check, sns_crossaccount, looks at the resource policy attached to each topic and reports any statement that hands access to a principal outside the account that owns the topic.


What this check detects

Every SNS topic can carry an access policy, a JSON document that controls who can call actions like sns:Publish, sns:Subscribe, and sns:Receive. The check parses that policy and inspects the Principal and Condition blocks of each Allow statement.

It raises a finding when a statement grants access to an AWS account ID, role, or user that does not match the account ID of the topic itself. In practice that means one of the following:

  • A Principal referencing another account, for example "AWS": "arn:aws:iam::222233334444:root", where 222233334444 is not your account.
  • A wildcard Principal ("AWS": "*") with no Condition restricting the source account or organization.
  • A condition that allows a broad set of accounts, such as a aws:SourceArn pattern with a wildcard in the account position.

Note: Cross-account access is not automatically wrong. Plenty of valid designs publish from a central app account into security or analytics accounts. The check exists to make sure every one of those grants is deliberate, scoped, and reviewed rather than left over from a copy-pasted policy.


Why it matters

The blast radius depends on which action you exposed. Two of them carry real risk.

Unrestricted subscribe leaks your data

If an external account can call sns:Subscribe, it can attach its own endpoint, an SQS queue, an HTTPS URL, or an email address, to your topic. From that moment, every message you publish flows to the attacker. If the topic carries order events, password reset notifications, billing data, or internal alerts, you have an ongoing data exfiltration channel that produces no errors and no obvious symptoms.

Unrestricted publish enables spoofing and abuse

If an external account can call sns:Publish, it can inject arbitrary messages into your topic. Downstream consumers, a Lambda that processes payments, a queue that drives provisioning, an SMS gateway, generally trust whatever lands on the topic. An attacker who can publish can trigger false workflows, send phishing SMS messages billed to your account, or flood subscribers to run up cost and noise.

Warning: SNS SMS and mobile push are billed per message. A wide-open publish policy on an SMS topic is both a security incident and a direct path to a surprise bill. Attackers have used exposed SNS topics to pump traffic to premium-rate numbers.

The reason this slips through is that SNS policies are easy to over-grant. Developers often start with "Principal": "*" to make a cross-account subscription work during testing, then forget to tighten it. The architecture keeps functioning, so nobody notices until an audit, or an incident, finds it.


How to fix it

Start by reading the current policy so you understand what is actually being granted before you change anything.

aws sns get-topic-attributes \
  --topic-arn arn:aws:sns:us-east-1:111122223333:order-events \
  --query 'Attributes.Policy' \
  --output text | jq .

Look at each statement. You are checking three things: who the Principal is, which Action values are allowed, and whether a Condition scopes the access. A safe cross-account statement names the exact external account and pins the source with a condition.

Option 1: Tighten the policy to a specific account

If the cross-account access is legitimate, replace any wildcard with the precise principal and add a condition. Here is a policy that lets one named account subscribe, and nothing else:

{
  "Version": "2012-10-17",
  "Id": "order-events-policy",
  "Statement": [
    {
      "Sid": "AllowAnalyticsAccountSubscribe",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::222233334444:root"
      },
      "Action": "sns:Subscribe",
      "Resource": "arn:aws:sns:us-east-1:111122223333:order-events",
      "Condition": {
        "StringEquals": {
          "sns:Protocol": "sqs"
        }
      }
    }
  ]
}

Save that to policy.json and apply it:

aws sns set-topic-attributes \
  --topic-arn arn:aws:sns:us-east-1:111122223333:order-events \
  --attribute-name Policy \
  --attribute-value file://policy.json

Danger: set-topic-attributes replaces the entire policy. If you submit a partial document, you will silently drop any statements you left out, which can break existing cross-account subscribers or, worse, lock out the publishers you depend on. Always start from the current policy and edit it.

Option 2: Remove the cross-account grant entirely

If the external access is not needed, drop the offending statement and apply a policy that only permits the owning account. The simplest safe baseline is the default SNS policy that scopes everything to your account ID with the aws:SourceOwner condition:

{
  "Version": "2012-10-17",
  "Id": "default-policy",
  "Statement": [
    {
      "Sid": "OwnerAccountOnly",
      "Effect": "Allow",
      "Principal": { "AWS": "*" },
      "Action": [
        "sns:Publish",
        "sns:Subscribe",
        "sns:GetTopicAttributes",
        "sns:SetTopicAttributes"
      ],
      "Resource": "arn:aws:sns:us-east-1:111122223333:order-events",
      "Condition": {
        "StringEquals": {
          "aws:SourceOwner": "111122223333"
        }
      }
    }
  ]
}

Note that even though the Principal is * here, the aws:SourceOwner condition restricts callers to your own account, so it is not actually public.

Option 3: Fix it in infrastructure as code

If the topic is managed by Terraform, change the policy at the source so it does not drift back on the next apply. Define the allowed account as a variable and use a data source to build the document:

data "aws_iam_policy_document" "order_events" {
  statement {
    sid     = "AllowAnalyticsAccountSubscribe"
    effect  = "Allow"
    actions = ["sns:Subscribe"]

    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::${var.analytics_account_id}:root"]
    }

    resources = [aws_sns_topic.order_events.arn]

    condition {
      test     = "StringEquals"
      variable = "sns:Protocol"
      values   = ["sqs"]
    }
  }
}

resource "aws_sns_topic_policy" "order_events" {
  arn    = aws_sns_topic.order_events.arn
  policy = data.aws_iam_policy_document.order_events.json
}

Tip: Keep the list of trusted account IDs in one variable or a shared module input. When someone needs to add or revoke a cross-account consumer, it is a one-line change in version control with a reviewable diff, instead of a console edit nobody can trace later.


How to prevent it from happening again

Manual reviews catch this once. Automation catches it every time. Layer a few controls so a loose policy cannot reach production.

Block public and unscoped principals with an SCP

An organization-wide Service Control Policy can deny any attempt to set an SNS policy that lacks an account scope. While SCPs cannot inspect policy contents directly, you can pair them with AWS Config rules and detective controls. At minimum, deny topic policy changes outside your CI role so the only path to production is through a reviewed pipeline.

Catch it in CI before merge

Run a policy-as-code scan on every pull request. With Checkov, the relevant rule flags SNS policies that allow public or cross-account access without conditions:

checkov -d ./infra --framework terraform \
  --check CKV_AWS_169

Wire that into your pipeline so a failing check blocks the merge:

# .github/workflows/iac-scan.yml (excerpt)
- name: Scan IaC for SNS misconfigurations
  run: |
    pip install checkov
    checkov -d ./infra --framework terraform --soft-fail-on LOW

Detect drift at runtime

IaC scanning misses topics created by hand or by another tool. Use AWS Config with the managed rule or a custom rule that evaluates topic policies, and route findings to a Security Hub or Lensix dashboard. Lensix runs sns_crossaccount continuously, so a topic that someone edits in the console at 2 a.m. shows up as a finding rather than waiting for the next audit cycle.

Tip: Pair the SNS check with a notification rule so new cross-account grants page the right team. A grant that appears outside a deploy window is a strong signal that someone changed something by hand, which is exactly when you want eyes on it.


Best practices

  • Name the account, never wildcard it. Always reference a specific account ARN in the Principal. A * principal without a tight condition is the root cause of nearly every finding here.
  • Add a condition even when you name a principal. Use aws:SourceOwner, aws:SourceArn, or aws:PrincipalOrgID to constrain access further. Defense in depth means a single misedit does not open the door.
  • Scope to org, not account, when it fits. If you trust an entire organization, use aws:PrincipalOrgID so you do not maintain a growing list of account IDs, while still excluding everyone outside the org.
  • Separate publish and subscribe permissions. An analytics account usually only needs Subscribe. A producer account only needs Publish. Granting both because it was easier is how topics end up over-permissioned.
  • Encrypt sensitive topics. Enable SSE with a customer-managed KMS key and restrict the key policy too. Cross-account access then also requires KMS permissions, which adds a second gate an attacker must pass.
  • Review cross-account grants on a schedule. Accounts get decommissioned and partners change. A grant that was valid last year may point at an account someone else now controls.

Treat every cross-account SNS grant as a contract between two accounts. If you cannot name the account, the action, and the reason in one sentence, the grant is too broad.

Cross-account SNS access is a normal part of multi-account architecture. The problem is never that you shared a topic, it is that you shared it more widely than you meant to. Scope each grant tightly, validate it in CI, and watch for drift, and this check stays green without slowing anyone down.