Find SQS Queues and SNS Topics Without Encryption Enabled

A long row of metal mailboxes with numbered doors on a wall

Photo by KC Shum on Unsplash

SQS queue encryption is not enabled when GetQueueAttributes returns neither SqsManagedSseEnabled set to true nor a KmsMasterKeyId. For SNS, a topic is unencrypted when GetTopicAttributes has no KmsMasterKeyId. The script below checks both in each Region and, with --apply, turns on SSE-SQS for queues that have no encryption at all.

New SQS queues have been encrypted with SQS-managed keys by default since October 2022, so an unencrypted queue today is usually an older queue, or one created with encryption switched off on purpose. SNS topics are different: they have no default encryption, so every topic created without a key is still a finding.

This example is for engineers who need to close “SQS queue encryption not enabled” findings from Security Hub or an audit, and want to know which key each queue and topic uses before changing anything. It reads by default and only writes when you pass --apply.

What does SQS queue encryption not enabled mean?

Amazon SQS offers two kinds of server-side encryption (SSE), and Amazon SNS offers one. The SQS encryption at rest guide explains the SQS options in full:

State How it shows in the API What it means
Not encrypted No KmsMasterKeyId; SqsManagedSseEnabled is false or missing Message bodies are stored without SSE. This is the finding
SSE-SQS (queues only) SqsManagedSseEnabled: "true" SQS-owned keys, no key to manage and no extra charge
SSE-KMS, AWS managed key KmsMasterKeyId: "alias/aws/sqs" or "alias/aws/sns" One key per account and Region; you can’t edit its key policy
SSE-KMS, customer managed key KmsMasterKeyId set to your key or alias You write the key policy, so AWS services and other accounts can be allowed. KMS charges apply

In every case, SSE encrypts the message body only. Queue and topic names, attributes, message IDs, timestamps and message attributes stay unencrypted, so don’t put secrets in message attributes.

Why does the AWS managed key cause delivery failures?

AWS services that send to your queues and topics, such as S3 event notifications, EventBridge (CloudWatch Events) rules and CloudWatch alarms, need kms:GenerateDataKey and kms:Decrypt on the key. You grant that in a key policy, and you can’t modify the key policy of alias/aws/sqs or alias/aws/sns. Both services’ docs say to use a customer managed key for these event sources; the SNS key management guide lists the service principals for each one. The SQS guide adds that a queue encrypted with the default key can’t invoke a Lambda function in a different account. The script marks every queue or topic on an AWS managed key so you can check who publishes to it.

What does the script do?

  1. Lists queuespaginateListQueues in each Region from --regions (default: your profile’s Region).
  2. Reads queue encryptionGetQueueAttributes for SqsManagedSseEnabled and KmsMasterKeyId only, not the whole attribute set.
  3. Lists and reads topicspaginateListTopics, then GetTopicAttributes for each topic’s KmsMasterKeyId.
  4. ClassifiesNot encrypted, SSE-SQS, SSE-KMS with the AWS managed key, or SSE-KMS with your own key.
  5. Optionally fixes queuesWith --apply, calls SetQueueAttributes with SqsManagedSseEnabled: "true" on unencrypted queues. Topics are never changed, because they need a key and key policy you choose.

Prerequisites

  • Node.js 18 or later, npm and tsx, plus @aws-sdk/client-sqs and @aws-sdk/client-sns.
  • An AWS profile, set up as in the guide to AWS SDK v3 credential providers such as fromIni and fromSSO.
  • For --apply: a list of producers that send without signing. Encrypted queues reject anonymous requests (more on that below).

Which IAM permissions does it need?

sqs-sns-encryption-audit-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadQueuesAndTopics",
      "Effect": "Allow",
      "Action": [
        "sqs:ListQueues",
        "sqs:GetQueueAttributes",
        "sns:ListTopics",
        "sns:GetTopicAttributes"
      ],
      "Resource": "*"
    },
    {
      "Sid": "EnableSseSqsOnlyWithApply",
      "Effect": "Allow",
      "Action": "sqs:SetQueueAttributes",
      "Resource": "arn:aws:sqs:*:123456789012:*"
    }
  ]
}

Replace 123456789012 with your account ID, and drop the second statement for report-only runs. SetQueueAttributes can also change a queue’s policy, so don’t hand the write statement to a broad group. The IAM policy generator for TypeScript code derives the same list from the script.

The script to find unencrypted SQS queues and SNS topics

find-unencrypted-sqs-queues-and-sns-topics.ts

// find-unencrypted-sqs-queues-and-sns-topics.ts
// Lists SQS queues and SNS topics in each Region with their server-side encryption setting.
// Report only by default. With --apply, turns on SSE-SQS for queues that have no encryption at all.
// SNS topics are never changed: they need a KMS key you choose and a key policy you write.
// Usage:
//   npx tsx find-unencrypted-sqs-queues-and-sns-topics.ts [--regions us-east-1,eu-west-1] [--csv sse.csv] [--apply]
import { writeFileSync } from "node:fs";
import { GetQueueAttributesCommand, SetQueueAttributesCommand, SQSClient, paginateListQueues } from "@aws-sdk/client-sqs";
import { GetTopicAttributesCommand, SNSClient, paginateListTopics } from "@aws-sdk/client-sns";

const args = process.argv.slice(2);
const flag = (name: string): string | undefined => {
  const i = args.indexOf(name);
  return i >= 0 ? args[i + 1] : undefined;
};
const apply = args.includes("--apply");
const csvPath = flag("--csv");
const regions = (flag("--regions") ?? process.env.AWS_REGION ?? "us-east-1")
  .split(",")
  .map((r) => r.trim())
  .filter(Boolean);

interface Row {
  Region: string;
  Type: "SQS" | "SNS";
  Name: string;
  Encryption: string;
  Finding: string;
}

// SSE-KMS with the AWS managed key works for your own producers, but AWS services such as S3 event
// notifications or EventBridge need a customer managed key whose policy names them.
function describeKey(keyId: string, awsManagedAlias: string): { enc: string; finding: string } {
  if (keyId === awsManagedAlias) {
    return { enc: `SSE-KMS (${awsManagedAlias})`, finding: "ok; AWS service publishers need a customer managed key" };
  }
  return { enc: "SSE-KMS (customer key)", finding: "ok" };
}

async function scanQueues(region: string): Promise<Row[]> {
  const sqs = new SQSClient({ region });
  const rows: Row[] = [];
  for await (const page of paginateListQueues({ client: sqs }, {})) {
    for (const url of page.QueueUrls ?? []) {
      const name = url.split("/").pop() ?? url;
      const { Attributes: a = {} } = await sqs.send(
        new GetQueueAttributesCommand({ QueueUrl: url, AttributeNames: ["SqsManagedSseEnabled", "KmsMasterKeyId"] }),
      );
      if (a.KmsMasterKeyId) {
        rows.push({ Region: region, Type: "SQS", Name: name, ...toRow(describeKey(a.KmsMasterKeyId, "alias/aws/sqs")) });
      } else if (a.SqsManagedSseEnabled === "true") {
        rows.push({ Region: region, Type: "SQS", Name: name, Encryption: "SSE-SQS", Finding: "ok" });
      } else {
        let finding = "NOT ENCRYPTED";
        if (apply) {
          await sqs.send(new SetQueueAttributesCommand({ QueueUrl: url, Attributes: { SqsManagedSseEnabled: "true" } }));
          finding = "NOT ENCRYPTED -> SSE-SQS enabled";
        }
        rows.push({ Region: region, Type: "SQS", Name: name, Encryption: "none", Finding: finding });
      }
    }
  }
  return rows;
}

async function scanTopics(region: string): Promise<Row[]> {
  const sns = new SNSClient({ region });
  const rows: Row[] = [];
  for await (const page of paginateListTopics({ client: sns }, {})) {
    for (const topic of page.Topics ?? []) {
      const arn = topic.TopicArn ?? "";
      const { Attributes: a = {} } = await sns.send(new GetTopicAttributesCommand({ TopicArn: arn }));
      const name = arn.split(":").pop() ?? arn;
      if (a.KmsMasterKeyId) {
        rows.push({ Region: region, Type: "SNS", Name: name, ...toRow(describeKey(a.KmsMasterKeyId, "alias/aws/sns")) });
      } else {
        rows.push({ Region: region, Type: "SNS", Name: name, Encryption: "none", Finding: "NOT ENCRYPTED (fix manually)" });
      }
    }
  }
  return rows;
}

function toRow(r: { enc: string; finding: string }): { Encryption: string; Finding: string } {
  return { Encryption: r.enc, Finding: r.finding };
}

function toCsv(rows: Row[]): string {
  const cols = Object.keys(rows[0] ?? {}) as (keyof Row)[];
  const cell = (v: string) => `"${v.replace(/"/g, '""')}"`;
  return [cols.join(","), ...rows.map((r) => cols.map((c) => cell(r[c])).join(","))].join("\n") + "\n";
}

async function main(): Promise<void> {
  const rows: Row[] = [];
  for (const region of regions) {
    for (const scan of [scanQueues, scanTopics]) {
      try {
        rows.push(...(await scan(region)));
      } catch (err) {
        console.error(`${region} ${scan.name}: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
      }
    }
  }
  const findings = rows.filter((r) => r.Finding !== "ok");
  if (findings.length) console.table(findings);
  const none = rows.filter((r) => r.Encryption === "none");
  console.log(`${rows.length} queues and topics checked, ${none.length} without encryption`);
  if (csvPath && rows.length) {
    writeFileSync(csvPath, toCsv(rows));
    console.log(`Wrote ${rows.length} rows to ${csvPath}`);
  }
  console.log(apply ? "Applied SSE-SQS to unencrypted queues only. Topics were not changed." : "Report only: nothing was modified. Add --apply to enable SSE-SQS on unencrypted queues.");
}

main().catch((err) => {
  console.error(err);
  process.exit(1);
});

How do you run it?

Terminal

npm install @aws-sdk/client-sqs @aws-sdk/client-sns
npm install --save-dev tsx typescript @types/node

# Report for two Regions
AWS_PROFILE=readonly npx tsx find-unencrypted-sqs-queues-and-sns-topics.ts --regions us-east-1,eu-west-1 --csv sse.csv

# Turn on SSE-SQS for unencrypted queues in one Region
AWS_PROFILE=ops npx tsx find-unencrypted-sqs-queues-and-sns-topics.ts --regions us-east-1 --apply

Sample output

Output

┌─────────┬─────────────┬───────┬────────────────────┬───────────────────────────┬──────────────────────────────────────────────────────────┐
│ (index) │ Region      │ Type  │ Name               │ Encryption                │ Finding                                                  │
├─────────┼─────────────┼───────┼────────────────────┼───────────────────────────┼──────────────────────────────────────────────────────────┤
│ 0       │ 'us-east-1' │ 'SQS' │ 'orders-2019'      │ 'none'                    │ 'NOT ENCRYPTED'                                          │
│ 1       │ 'us-east-1' │ 'SQS' │ 's3-upload-events' │ 'SSE-KMS (alias/aws/sqs)' │ 'ok; AWS service publishers need a customer managed key' │
│ 2       │ 'us-east-1' │ 'SNS' │ 'billing-alarms'   │ 'none'                    │ 'NOT ENCRYPTED (fix manually)'                           │
│ 3       │ 'eu-west-1' │ 'SNS' │ 'deploy-events'    │ 'SSE-KMS (alias/aws/sns)' │ 'ok; AWS service publishers need a customer managed key' │
└─────────┴─────────────┴───────┴────────────────────┴───────────────────────────┴──────────────────────────────────────────────────────────┘
37 queues and topics checked, 2 without encryption
Wrote 37 rows to sse.csv
Report only: nothing was modified. Add --apply to enable SSE-SQS on unencrypted queues.

Names are illustrative. Two rows need a closer look before you touch them. s3-upload-events is on the AWS managed key and its name suggests S3 notifications feed it; if messages stopped arriving after someone enabled encryption, that’s the reason. billing-alarms is a topic CloudWatch alarms publish to, so it needs a customer managed key that allows cloudwatch.amazonaws.com, not aws/sns.

What changes when you turn on SSE-SQS?

Enabling SSE-SQS is usually safe, but four details catch people out:

  • Only new messages are encrypted. SQS doesn’t encrypt messages already in the queue, and turning encryption off later doesn’t decrypt messages that are already encrypted.
  • Anonymous requests stop working. With SSE enabled, anonymous SendMessage and ReceiveMessage requests are rejected, and every request must use HTTPS and Signature Version 4. Anything using the AWS SDK already does.
  • It takes up to 60 seconds. Most attribute changes need that long to propagate through SQS.
  • A queue has one SSE option. It’s SSE-SQS or SSE-KMS, not both. To move a queue to your own key later, set KmsMasterKeyId instead.

For a topic, the fix is manual. Create or pick a customer managed key, add the publishing service principals to its key policy, then run aws sns set-topic-attributes --topic-arn <arn> --attribute-name KmsMasterKeyId --attribute-value alias/sns-events. Your own publishers need kms:GenerateDataKey* and kms:Decrypt on that key; the guide to publish an SNS message with AWS SDK v3 in TypeScript shows the publishing side, and sending and receiving SQS messages with AWS SDK v3 shows the consumer side. Once a new key is in place, turn on automatic rotation for KMS keys that lack it.

Warning: if you replace a queue’s KMS key, keep the old key enabled until the queue has drained. Messages encrypted under it still need it to be decrypted.

Encryption is one of three queue checks worth running together. Also find public SNS topics and SQS queues, because a resource policy open to everyone matters more than encryption at rest, and find SQS queues without a dead-letter queue. Whoever reads a dead-letter queue also needs kms:Decrypt on the source queue’s key, because moved messages keep their original encryption.

Troubleshooting

  • InvalidSecurity from SetQueueAttributes. The SQS API returns it when a request isn’t made over HTTPS or isn’t signed with SigV4. Check for a custom endpoint using http://.
  • S3 or EventBridge deliveries stop after you add a KMS key. The key is aws/sqs or aws/sns, or its policy doesn’t allow the service principal. Switch to a customer managed key with that principal in the policy.
  • A topic reports AuthorizationError. Your profile lacks sns:GetTopicAttributes on it, often because a topic policy or SCP denies it. The guide to troubleshoot AWS IAM access denied errors walks through the checks.
  • Queues from other Regions are missing. ListQueues is Regional; pass every Region you use with --regions.

Ask ChatWithCloud instead

For a one-off check, ask ChatWithCloud “Which SQS queues in us-east-1 don’t have server-side encryption?” It writes AWS SDK for JavaScript v2 code, runs it locally with your profile and explains the answer, one profile and Region per session; how ChatWithCloud answers AWS questions from your terminal covers the loop. It can be wrong, and it runs changes without a confirmation step, so connect ChatWithCloud with a read-only AWS profile for questions like this one.

Frequently asked questions

Are SQS queues encrypted by default?

Yes, for queues created since October 2022 through the HTTPS endpoint: they get SSE-SQS by default. Older queues, and queues created with encryption turned off, stay unencrypted until you change them.

How do I enable encryption on an existing SQS queue?

Run aws sqs set-queue-attributes --queue-url <url> --attributes SqsManagedSseEnabled=true for SSE-SQS, or set KmsMasterKeyId to a KMS key for SSE-KMS. Only messages sent afterwards are encrypted.

Are SNS topics encrypted by default?

No. An SNS topic uses server-side encryption only when you set KmsMasterKeyId to a KMS key, either the AWS managed alias/aws/sns key or your own.

Should I use SSE-SQS or SSE-KMS?

Use SSE-SQS when you don’t need key-level control. Use SSE-KMS with a customer managed key when you need your own key policy, or when AWS services or other accounts send to the queue.

Related guides

Ask your AWS account in plain English

Your first 15 runs are free, with no OpenAI key needed.

npx chatwithcloud