Find Unencrypted RDS Instances and Aurora Clusters

A closed metal server cabinet with a lock on its door in a dimly lit data centre

Photo by iMattSmart on Unsplash

To find unencrypted RDS instances, call DescribeDBClusters and DescribeDBInstances in each Region and flag every database where StorageEncrypted is false. For the encrypted ones, StorageEncryptionType and a KMS DescribeKey call tell you whether an AWS owned key, the AWS managed aws/rds key or your own key protects them. Encryption can’t be turned on in place.

Encryption at rest is one of the first things an auditor asks about, and one of the few RDS settings you can’t fix with a quick modify. A database created without it stays that way until you rebuild it from an encrypted snapshot, and the older the account, the more likely a few of those are still running. Caches have the same catch: ElastiCache at-rest encryption on node-based clusters is also set only at creation, and the script to find ElastiCache without encryption at rest, in transit or AUTH lists the ones that need a rebuild.

This example is for engineers who need a list they can act on: every RDS DB instance and Aurora or Multi-AZ DB cluster, whether it’s encrypted, and which kind of key sits behind it. The script to find unencrypted RDS instances only reads, so a read-only profile is enough. It goes deeper on keys and migration than the broader check to find RDS instances without automated backups or encryption.

Which encryption states can an RDS database be in?

There are now four answers, not two. The API returns a StorageEncryptionType field next to StorageEncrypted for both DB instances and DB clusters:

State How it shows in the API What it means for you
Not encrypted StorageEncrypted: false, type none Storage, logs, automated backups, snapshots and replicas are all unencrypted. This is the finding
AWS owned key type sse-rds The default for Aurora clusters created on or after 18 February 2026. You can’t see or manage the key
AWS managed key type sse-kms, key manager AWS (aws/rds) One key per account and Region, visible but not configurable. You can’t share snapshots encrypted with it
Customer managed key type sse-kms, key manager CUSTOMER You control the key policy, rotation and cross-account access. KMS charges apply

The last column matters more than it looks. If a snapshot is encrypted with aws/rds, you can’t share it with another account, which blocks a common disaster-recovery pattern. And if someone disables a customer managed key, RDS loses access to it and the database goes into the inaccessible-encryption-credentials-recoverable state. The script flags keys that are Disabled or PendingDeletion for that reason. The RDS encryption limitations and the Aurora encryption guide list the rules in full.

What does the script do?

  1. Picks RegionsYour profile’s Region, a list with --regions, or every Region with --all-regions (one DescribeRegions call).
  2. Reads clusterspaginateDescribeDBClusters covers Aurora and Multi-AZ DB clusters. Encryption is a cluster setting, so member instances aren’t listed again.
  3. Reads standalone instancespaginateDescribeDBInstances, skipping cluster members and marking read replicas.
  4. Classifies the keyOne cached DescribeKey per key ARN returns KeyManager and KeyState.
  5. Reports onlyPrints findings, writes a CSV with --csv, and with --require-cmk also flags databases that don’t use your own key.

Prerequisites

  • Node.js 18 or later, npm and tsx, plus @aws-sdk/client-rds, @aws-sdk/client-kms and @aws-sdk/client-ec2.
  • An AWS profile, configured as in the guide to AWS SDK v3 credential providers such as fromIni and fromSSO.
  • Your encryption standard written down: is aws/rds acceptable, or do production databases need a customer managed key?

Which IAM permissions does it need?

rds-encryption-audit-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadDatabases",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeRegions",
        "rds:DescribeDBInstances",
        "rds:DescribeDBClusters"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ReadKeyMetadata",
      "Effect": "Allow",
      "Action": "kms:DescribeKey",
      "Resource": "arn:aws:kms:*:123456789012:key/*"
    }
  ]
}

Replace 123456789012 with your account ID. If a database uses a key from another account, DescribeKey needs that key’s policy to allow you, otherwise the row shows AccessDeniedException. To check the list against the code, paste the script into the IAM policy generator for TypeScript code.

The script to find unencrypted RDS instances

find-unencrypted-rds-instances.ts

// find-unencrypted-rds-instances.ts
// Report only. Lists RDS DB instances and Aurora / Multi-AZ DB clusters in each Region, shows whether
// storage is encrypted and with which kind of KMS key, and flags unencrypted databases and keys that
// are disabled or pending deletion.
// Usage:
//   npx tsx find-unencrypted-rds-instances.ts [--regions us-east-1,eu-west-1 | --all-regions] [--require-cmk] [--csv rds-encryption.csv]
import { writeFileSync } from "node:fs";
import { DescribeRegionsCommand, EC2Client } from "@aws-sdk/client-ec2";
import { DescribeKeyCommand, KMSClient } from "@aws-sdk/client-kms";
import { RDSClient, paginateDescribeDBClusters, paginateDescribeDBInstances } from "@aws-sdk/client-rds";

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 requireCmk = args.includes("--require-cmk"); // also flag the AWS managed aws/rds key and AWS owned keys
const csvPath = flag("--csv");

interface Row {
  Region: string;
  Kind: string;
  Name: string;
  Engine: string;
  Created: string;
  Encryption: string;
  KeyState: string;
  Finding: string;
}

interface KeyInfo {
  manager: string; // "AWS" (aws/rds) or "CUSTOMER"
  state: string; // Enabled, Disabled, PendingDeletion, ...
}

async function regionList(): Promise<string[]> {
  const named = flag("--regions");
  if (named) return named.split(",").map((r) => r.trim()).filter(Boolean);
  if (!args.includes("--all-regions")) return [process.env.AWS_REGION ?? "us-east-1"];
  const ec2 = new EC2Client({ region: process.env.AWS_REGION ?? "us-east-1" });
  const { Regions = [] } = await ec2.send(new DescribeRegionsCommand({}));
  return Regions.map((r) => r.RegionName ?? "").filter(Boolean).sort();
}

async function scanRegion(region: string): Promise<Row[]> {
  const rds = new RDSClient({ region });
  const kms = new KMSClient({ region });
  const keys = new Map<string, KeyInfo>();

  // One DescribeKey per distinct key ARN. Needs kms:DescribeKey; without it the key shows as "unknown".
  const describeKey = async (keyId: string): Promise<KeyInfo> => {
    const cached = keys.get(keyId);
    if (cached) return cached;
    let info: KeyInfo = { manager: "unknown", state: "unknown" };
    try {
      const { KeyMetadata } = await kms.send(new DescribeKeyCommand({ KeyId: keyId }));
      info = { manager: KeyMetadata?.KeyManager ?? "unknown", state: KeyMetadata?.KeyState ?? "unknown" };
    } catch (err) {
      info = { manager: "unknown", state: err instanceof Error ? err.name : "error" };
    }
    keys.set(keyId, info);
    return info;
  };

  // Classify one database: storage encryption type first, then the key behind it.
  const classify = async (encrypted: boolean | undefined, type: string | undefined, keyId: string | undefined) => {
    if (!encrypted || type === "none") return { enc: "NONE", state: "", finding: "UNENCRYPTED" };
    if (type === "sse-rds") return { enc: "AWS owned key", state: "", finding: requireCmk ? "not a customer managed key" : "ok" };
    if (!keyId) return { enc: "encrypted (key not returned)", state: "", finding: "check manually" };
    const key = await describeKey(keyId);
    const enc = key.manager === "AWS" ? "aws/rds (AWS managed)" : key.manager === "CUSTOMER" ? "customer managed" : "unknown key";
    let finding = "ok";
    if (key.state === "Disabled" || key.state === "PendingDeletion") finding = `KEY ${key.state.toUpperCase()}`;
    else if (requireCmk && key.manager !== "CUSTOMER") finding = "not a customer managed key";
    return { enc, state: key.state, finding };
  };

  const rows: Row[] = [];
  const day = (d: Date | undefined) => (d ? d.toISOString().slice(0, 10) : "");

  // Aurora and Multi-AZ DB clusters: encryption is a cluster setting shared by every member instance.
  for await (const page of paginateDescribeDBClusters({ client: rds }, {})) {
    for (const c of page.DBClusters ?? []) {
      const r = await classify(c.StorageEncrypted, c.StorageEncryptionType, c.KmsKeyId);
      rows.push({
        Region: region,
        Kind: "cluster",
        Name: c.DBClusterIdentifier ?? "?",
        Engine: c.Engine ?? "?",
        Created: day(c.ClusterCreateTime),
        Encryption: r.enc,
        KeyState: r.state,
        Finding: r.finding,
      });
    }
  }

  // Standalone DB instances and read replicas. Cluster members are covered by the cluster row above.
  for await (const page of paginateDescribeDBInstances({ client: rds }, {})) {
    for (const db of page.DBInstances ?? []) {
      if (db.DBClusterIdentifier) continue;
      const r = await classify(db.StorageEncrypted, db.StorageEncryptionType, db.KmsKeyId);
      rows.push({
        Region: region,
        Kind: db.ReadReplicaSourceDBInstanceIdentifier ? "replica" : "instance",
        Name: db.DBInstanceIdentifier ?? "?",
        Engine: db.Engine ?? "?",
        Created: day(db.InstanceCreateTime),
        Encryption: r.enc,
        KeyState: r.state,
        Finding: r.finding,
      });
    }
  }
  return rows;
}

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 await regionList()) {
    try {
      rows.push(...(await scanRegion(region)));
    } catch (err) {
      console.error(`${region}: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
    }
  }
  const findings = rows.filter((r) => r.Finding !== "ok");
  console.table(findings.length ? findings : rows);
  const unencrypted = rows.filter((r) => r.Finding === "UNENCRYPTED").length;
  console.log(`${rows.length} databases checked, ${unencrypted} unencrypted, ${findings.length - unencrypted} other findings`);
  if (csvPath && rows.length) {
    writeFileSync(csvPath, toCsv(rows));
    console.log(`Wrote ${rows.length} rows to ${csvPath}`);
  }
  console.log("Report only: nothing was modified.");
}

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

How do you run it?

Terminal

npm install @aws-sdk/client-rds @aws-sdk/client-kms @aws-sdk/client-ec2
npm install --save-dev tsx typescript @types/node

# Every Region, CSV for the audit ticket
AWS_PROFILE=readonly npx tsx find-unencrypted-rds-instances.ts --all-regions --csv rds-encryption.csv

# Production standard: anything not on a customer managed key is a finding
AWS_PROFILE=readonly npx tsx find-unencrypted-rds-instances.ts --regions eu-west-1 --require-cmk

Sample output

Output

┌─────────┬─────────────┬────────────┬──────────────────┬─────────────────────┬──────────────┬────────────────────┬───────────────────┬───────────────────────┐
│ (index) │ Region      │ Kind       │ Name             │ Engine              │ Created      │ Encryption         │ KeyState          │ Finding               │
├─────────┼─────────────┼────────────┼──────────────────┼─────────────────────┼──────────────┼────────────────────┼───────────────────┼───────────────────────┤
│ 0       │ 'us-east-1' │ 'instance' │ 'legacy-mysql'   │ 'mysql'             │ '2019-03-14' │ 'NONE'             │ ''                │ 'UNENCRYPTED'         │
│ 1       │ 'us-east-1' │ 'replica'  │ 'legacy-mysql-r' │ 'mysql'             │ '2021-06-02' │ 'NONE'             │ ''                │ 'UNENCRYPTED'         │
│ 2       │ 'eu-west-1' │ 'cluster'  │ 'billing-aurora' │ 'aurora-postgresql' │ '2023-10-09' │ 'customer managed' │ 'PendingDeletion' │ 'KEY PENDINGDELETION' │
└─────────┴─────────────┴────────────┴──────────────────┴─────────────────────┴──────────────┴────────────────────┴───────────────────┴───────────────────────┘
14 databases checked, 2 unencrypted, 1 other findings
Wrote 14 rows to rds-encryption.csv
Report only: nothing was modified.

Names are illustrative. billing-aurora is the urgent row: its key is scheduled for deletion, and once a KMS key is deleted, nothing encrypted under it can be decrypted again, including the cluster’s backups. Cancel the deletion with aws kms cancel-key-deletion first, then deal with the unencrypted MySQL pair. The replica can’t be fixed separately, because RDS doesn’t allow an encrypted read replica of an unencrypted source.

How do you encrypt an existing RDS database?

You rebuild it from an encrypted snapshot. For an RDS DB instance:

  1. Snapshot the instanceaws rds create-db-snapshot --db-instance-identifier legacy-mysql --db-snapshot-identifier legacy-mysql-pre-encrypt, then aws rds wait db-snapshot-available with the same identifier.
  2. Copy it with a keyaws rds copy-db-snapshot --source-db-snapshot-identifier legacy-mysql-pre-encrypt --target-db-snapshot-identifier legacy-mysql-encrypted --kms-key-id alias/rds-prod. This is the only step that adds encryption.
  3. Restore a new instanceaws rds restore-db-instance-from-db-snapshot --db-instance-identifier legacy-mysql-v2 --db-snapshot-identifier legacy-mysql-encrypted, passing the same subnet group, security groups and parameter group as the original.
  4. Cut overPoint the application at the new endpoint, recreate read replicas from the encrypted instance, then delete the old one with a final snapshot.

Writes that land after the first snapshot are missing from the copy, so either stop writes for the whole run or keep the two in sync with AWS Database Migration Service and cut over once replication catches up. For Aurora, create a cluster snapshot and pass --kms-key-id to aws rds restore-db-cluster-from-snapshot, then add instances with aws rds create-db-instance --db-cluster-identifier. Aurora encrypts a restored cluster with an AWS owned key even if you don’t choose a key, but choose one deliberately: you can’t change the key later without another rebuild.

Warning: the new instance gets a new endpoint and a new resource ID. Anything that references the old identifier, such as IAM database authentication policies, alarms, proxies or DNS records, needs updating before you delete the original.

Afterwards, run the related checks: find public EBS and RDS snapshots so the old unencrypted snapshots aren’t shared, find and delete old RDS manual snapshots once the migration is signed off, and find KMS keys without automatic rotation and enable it for the new key. For the instances next to your databases, the script to find unencrypted EBS volumes and turn on default encryption covers the other half of encryption at rest.

Troubleshooting

  • A database shows as encrypted with an unknown key. DescribeKey was denied, usually because the key belongs to another account. The encryption status is still correct.
  • DocumentDB or Neptune clusters appear in the list. They share the RDS management API, so DescribeDBClusters returns them. The same rules apply; filter by the Engine column if you want RDS only.
  • StorageEncryptionType is missing. Older SDK versions don’t have the field. Update @aws-sdk/client-rds; the script still works from StorageEncrypted and the key lookup.
  • AccessDenied in one Region. A service control policy may block Regions you don’t use. The script prints the error and continues. The guide to troubleshoot IAM access denied errors step by step helps you tell the two apart.

Ask ChatWithCloud instead

For a quick answer, ask ChatWithCloud “Which RDS instances and Aurora clusters in eu-west-1 aren’t encrypted?” It writes AWS SDK for JavaScript v2 code, runs it on your machine with your profile and explains the result, one profile and Region per session; how ChatWithCloud runs AWS SDK code locally covers the loop. It can be wrong and runs generated code without a confirmation step, so connect ChatWithCloud through a read-only AWS profile. The guide to analyze your AWS security posture with an AI CLI has follow-up questions to try.

Frequently asked questions

How do I check if an RDS instance is encrypted?

Run aws rds describe-db-instances --query "DBInstances[].[DBInstanceIdentifier,StorageEncrypted]". For Aurora, check the cluster with describe-db-clusters, because encryption is set on the cluster.

Can I enable encryption on an existing RDS instance?

No. Snapshot it, copy the snapshot with a KMS key, and restore a new instance from the encrypted copy. You can’t turn encryption off on an encrypted database either.

Are new Aurora clusters encrypted by default?

Yes. Aurora clusters created on or after 18 February 2026 are encrypted with an AWS owned key unless you choose an AWS managed or customer managed key.

Should RDS use aws/rds or a customer managed key?

Use a customer managed key if you need cross-account snapshot sharing, your own key policy or audit control over key use. The AWS managed key is fine for single-account databases with no such need.

Related guides

Ask your AWS account in plain English

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

npx chatwithcloud