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?
- Picks RegionsYour profile’s Region, a list with
--regions, or every Region with--all-regions(oneDescribeRegionscall). - Reads clusters
paginateDescribeDBClusterscovers Aurora and Multi-AZ DB clusters. Encryption is a cluster setting, so member instances aren’t listed again. - Reads standalone instances
paginateDescribeDBInstances, skipping cluster members and marking read replicas. - Classifies the keyOne cached
DescribeKeyper key ARN returnsKeyManagerandKeyState. - Reports onlyPrints findings, writes a CSV with
--csv, and with--require-cmkalso 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-kmsand@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/rdsacceptable, or do production databases need a customer managed key?
Which IAM permissions does it need?
{
"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
// 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?
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
┌─────────┬─────────────┬────────────┬──────────────────┬─────────────────────┬──────────────┬────────────────────┬───────────────────┬───────────────────────┐
│ (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:
- Snapshot the instance
aws rds create-db-snapshot --db-instance-identifier legacy-mysql --db-snapshot-identifier legacy-mysql-pre-encrypt, thenaws rds wait db-snapshot-availablewith the same identifier. - Copy it with a key
aws 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. - Restore a new instance
aws 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. - 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.
DescribeKeywas 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
DescribeDBClustersreturns them. The same rules apply; filter by theEnginecolumn if you want RDS only. StorageEncryptionTypeis missing. Older SDK versions don’t have the field. Update@aws-sdk/client-rds; the script still works fromStorageEncryptedand the key lookup.AccessDeniedin 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