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
Principalreferencing another account, for example"AWS": "arn:aws:iam::222233334444:root", where222233334444is not your account. - A wildcard
Principal("AWS": "*") with noConditionrestricting the source account or organization. - A condition that allows a broad set of accounts, such as a
aws:SourceArnpattern 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, oraws:PrincipalOrgIDto 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:PrincipalOrgIDso 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 needsPublish. 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.

