Photo by Georg Bommeli on Unsplash
To find public EBS snapshots, call DescribeSnapshots with OwnerIds: ["self"] and RestorableByUserIds: ["all"] in each Region. For RDS, list manual snapshots and call DescribeDBSnapshotAttributes: a restore attribute containing all means anyone can restore it. Remove the all value to make it private.
A public snapshot is a full copy of a disk or database that any AWS account can restore. This example is for engineers who need to find public EBS snapshots and public RDS snapshots in their own account, remove the exposure, and stop it from happening again. The script uses AWS SDK for JavaScript v3, covers every enabled Region, and only changes permissions when you pass two explicit flags.
It’s one of our AWS SDK v3 practical examples and a natural next step after checking for public and private S3 buckets with the AWS SDK: buckets are the obvious leak, snapshots the forgotten one. For databases, the live endpoint matters as much as its snapshots; the script to find publicly accessible RDS instances checks it.
How do you find public EBS snapshots and RDS snapshots?
EBS and RDS record public sharing differently, so the script asks each service in its own terms:
| Snapshot type | How “public” is stored | How the script finds it |
|---|---|---|
| EBS snapshot | Create-volume permission granted to the group all |
DescribeSnapshots with OwnerIds: ["self"] and RestorableByUserIds: ["all"] |
| RDS DB snapshot (manual) | all in the restore attribute |
DescribeDBSnapshots with SnapshotType: "manual", then DescribeDBSnapshotAttributes |
| Aurora and other DB cluster snapshots (manual) | all in the restore attribute |
DescribeDBClusterSnapshots, then DescribeDBClusterSnapshotAttributes |
Only manual RDS snapshots can be shared, which is why the script skips automated ones. OwnerIds: ["self"] matters for EBS: without it, RestorableByUserIds: ["all"] returns public snapshots from every account in the Region, not just yours.
Two sharing rules limit what can be public at all. Public snapshots of encrypted EBS volumes aren’t supported, and an encrypted RDS snapshot can be shared only with specific account IDs, never with all. So every finding is an unencrypted snapshot, which is one more reason to find unencrypted EBS volumes and turn on default encryption and to find RDS instances without automated backups or encryption.
What is block public access for EBS snapshots?
Since November 2023, EBS has had an account-level, per-Region switch that blocks public sharing of snapshots. The AWS announcement of block public access for EBS snapshots describes the two modes and states there is no additional charge. The script reads the state with GetSnapshotBlockPublicAccessState:
block-all-sharing: no new public sharing, and snapshots that were already public are treated as private.block-new-sharing: no new public sharing, but snapshots that were already public stay public.unblocked: anyone with the right permission can make a snapshot public.
One subtlety: in block-all-sharing mode, the snapshot’s attributes still say it’s shared with all. The script will still report it, which is correct, because turning the block off would make it public again. The setting doesn’t cover RDS, and it doesn’t stop someone from sharing a public AMI whose snapshots can then be restored. The script to find public AMIs you’ve shared by mistake checks images and their own block public access setting.
Prerequisites
- Node.js 20 or later, npm and
tsx. - The
@aws-sdk/client-ec2and@aws-sdk/client-rdspackages. - A profile for the account you’re auditing; pass
--regionsto limit the scan to the Regions you use.
Which IAM permissions does it need?
Seven read actions for the report. The last statement is only for --make-private --apply. Replace 123456789012 with your account ID.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadEbsSnapshots",
"Effect": "Allow",
"Action": [
"ec2:DescribeRegions",
"ec2:DescribeSnapshots",
"ec2:GetSnapshotBlockPublicAccessState"
],
"Resource": "*"
},
{
"Sid": "ReadRdsSnapshots",
"Effect": "Allow",
"Action": [
"rds:DescribeDBSnapshots",
"rds:DescribeDBSnapshotAttributes",
"rds:DescribeDBClusterSnapshots",
"rds:DescribeDBClusterSnapshotAttributes"
],
"Resource": "*"
},
{
"Sid": "OptionalMakePrivate",
"Effect": "Allow",
"Action": [
"ec2:ModifySnapshotAttribute",
"rds:ModifyDBSnapshotAttribute",
"rds:ModifyDBClusterSnapshotAttribute"
],
"Resource": [
"arn:aws:ec2:*::snapshot/*",
"arn:aws:rds:*:123456789012:snapshot:*",
"arn:aws:rds:*:123456789012:cluster-snapshot:*"
]
}
]
}
ReadOnlyAccess covers the read part. To check what your audit role can really do before you run it, use the example to check the permissions of your currently assumed IAM role.
The script to find public EBS and RDS snapshots
// find-public-snapshots.ts
// Finds EBS snapshots, RDS DB snapshots and Aurora/Multi-AZ cluster snapshots owned by this
// account that are shared with everyone ("all"), in every enabled Region, and reports the
// EBS snapshot block public access state per Region. Read-only by default.
// With --make-private it lists what it would change (dry run); add --apply to remove public access.
// Usage: npx tsx find-public-snapshots.ts [--regions us-east-1,eu-west-1] [--make-private [--apply]]
import {
EC2Client,
DescribeRegionsCommand,
GetSnapshotBlockPublicAccessStateCommand,
ModifySnapshotAttributeCommand,
paginateDescribeSnapshots,
} from "@aws-sdk/client-ec2";
import {
RDSClient,
DescribeDBSnapshotAttributesCommand,
DescribeDBClusterSnapshotAttributesCommand,
ModifyDBSnapshotAttributeCommand,
ModifyDBClusterSnapshotAttributeCommand,
paginateDescribeDBSnapshots,
paginateDescribeDBClusterSnapshots,
} from "@aws-sdk/client-rds";
const makePrivate = process.argv.includes("--make-private");
const apply = process.argv.includes("--apply");
type Kind = "ebs" | "rds" | "rds-cluster";
type Finding = { region: string; kind: Kind; id: string; created: string; source: string };
function regionsArg(): string[] | undefined {
const i = process.argv.indexOf("--regions");
if (i === -1) return undefined;
const list = (process.argv[i + 1] ?? "").split(",").map((r) => r.trim()).filter(Boolean);
if (list.length === 0) throw new Error("--regions needs a comma-separated list");
return list;
}
async function listRegions(): Promise<string[]> {
const explicit = regionsArg();
if (explicit) return explicit;
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((r): r is string => Boolean(r)).sort();
}
const day = (d?: Date) => d?.toISOString().slice(0, 10) ?? "?";
async function scanRegion(region: string, findings: Finding[]): Promise<string> {
const ec2 = new EC2Client({ region, maxAttempts: 5 });
const rds = new RDSClient({ region, maxAttempts: 5 });
// EBS: snapshots we own that grant create-volume permission to the "all" group.
const ebsPages = paginateDescribeSnapshots(
{ client: ec2 },
{ OwnerIds: ["self"], RestorableByUserIds: ["all"] },
);
for await (const page of ebsPages) {
for (const s of page.Snapshots ?? []) {
findings.push({ region, kind: "ebs", id: s.SnapshotId ?? "?", created: day(s.StartTime), source: s.VolumeId ?? "-" });
}
}
// RDS: manual DB snapshots whose "restore" attribute contains "all".
for await (const page of paginateDescribeDBSnapshots({ client: rds }, { SnapshotType: "manual" })) {
for (const s of page.DBSnapshots ?? []) {
if (!s.DBSnapshotIdentifier) continue;
const { DBSnapshotAttributesResult: r } = await rds.send(
new DescribeDBSnapshotAttributesCommand({ DBSnapshotIdentifier: s.DBSnapshotIdentifier }),
);
const restore = r?.DBSnapshotAttributes?.find((a) => a.AttributeName === "restore");
if (restore?.AttributeValues?.includes("all")) {
findings.push({ region, kind: "rds", id: s.DBSnapshotIdentifier, created: day(s.SnapshotCreateTime), source: s.DBInstanceIdentifier ?? "-" });
}
}
}
// Aurora and Multi-AZ DB clusters: manual cluster snapshots, same attribute.
for await (const page of paginateDescribeDBClusterSnapshots({ client: rds }, { SnapshotType: "manual" })) {
for (const s of page.DBClusterSnapshots ?? []) {
if (!s.DBClusterSnapshotIdentifier) continue;
const { DBClusterSnapshotAttributesResult: r } = await rds.send(
new DescribeDBClusterSnapshotAttributesCommand({ DBClusterSnapshotIdentifier: s.DBClusterSnapshotIdentifier }),
);
const restore = r?.DBClusterSnapshotAttributes?.find((a) => a.AttributeName === "restore");
if (restore?.AttributeValues?.includes("all")) {
findings.push({ region, kind: "rds-cluster", id: s.DBClusterSnapshotIdentifier, created: day(s.SnapshotCreateTime), source: s.DBClusterIdentifier ?? "-" });
}
}
}
const { State } = await ec2.send(new GetSnapshotBlockPublicAccessStateCommand({}));
return State ?? "unknown";
}
async function removePublicAccess(f: Finding): Promise<void> {
if (f.kind === "ebs") {
await new EC2Client({ region: f.region }).send(
new ModifySnapshotAttributeCommand({
SnapshotId: f.id,
Attribute: "createVolumePermission",
OperationType: "remove",
GroupNames: ["all"],
}),
);
} else if (f.kind === "rds") {
await new RDSClient({ region: f.region }).send(
new ModifyDBSnapshotAttributeCommand({ DBSnapshotIdentifier: f.id, AttributeName: "restore", ValuesToRemove: ["all"] }),
);
} else {
await new RDSClient({ region: f.region }).send(
new ModifyDBClusterSnapshotAttributeCommand({ DBClusterSnapshotIdentifier: f.id, AttributeName: "restore", ValuesToRemove: ["all"] }),
);
}
}
async function main(): Promise<void> {
const findings: Finding[] = [];
const blockState: { region: string; ebsBlockPublicAccess: string }[] = [];
for (const region of await listRegions()) {
const state = await scanRegion(region, findings);
blockState.push({ region, ebsBlockPublicAccess: state });
}
console.table(blockState);
console.table(findings);
console.log(`${findings.length} public snapshots found.`);
if (!makePrivate) return;
console.log(`\n${apply ? "Removing" : "Dry run: would remove"} public access from ${findings.length} snapshots:`);
for (const f of findings) {
console.log(` ${f.region} ${f.kind} ${f.id}`);
if (apply) await removePublicAccess(f);
}
if (!apply && findings.length > 0) console.log("Re-run with --make-private --apply to make the change.");
}
main().catch((err: unknown) => {
console.error(err);
process.exit(1);
});
With --make-private --apply, EBS snapshots get ModifySnapshotAttribute with OperationType: "remove" and GroupNames: ["all"]; RDS snapshots get ValuesToRemove: ["all"] on the restore attribute. Removing all leaves any sharing with specific account IDs in place, so a snapshot you deliberately share with a partner account keeps working. Making a snapshot private rather than deleting it keeps it as a backup; to check that every in-use volume has a recent one, run the script to find EBS volumes without snapshots.
How do you run it?
npm install @aws-sdk/client-ec2 @aws-sdk/client-rds
npm install --save-dev tsx typescript
# Report: every enabled Region
AWS_PROFILE=security-audit npx tsx find-public-snapshots.ts
# Dry run: what would be made private
AWS_PROFILE=ops-admin npx tsx find-public-snapshots.ts --make-private
# Make the change
AWS_PROFILE=ops-admin npx tsx find-public-snapshots.ts --make-private --apply
# Then block new public sharing of EBS snapshots in each Region you use
aws ec2 enable-snapshot-block-public-access --state block-all-sharing --region us-east-1
Sample output
┌─────────┬─────────────┬──────────────────────┐
│ (index) │ region │ ebsBlockPublicAccess │
├─────────┼─────────────┼──────────────────────┤
│ 0 │ 'eu-west-1' │ 'unblocked' │
│ 1 │ 'us-east-1' │ 'block-new-sharing' │
└─────────┴─────────────┴──────────────────────┘
┌─────────┬─────────────┬───────────────┬──────────────────────────┬──────────────┬─────────────────────────┐
│ (index) │ region │ kind │ id │ created │ source │
├─────────┼─────────────┼───────────────┼──────────────────────────┼──────────────┼─────────────────────────┤
│ 0 │ 'us-east-1' │ 'ebs' │ 'snap-0a1b2c3d4e5f60718' │ '2024-03-11' │ 'vol-049df61146c4d7901' │
│ 1 │ 'us-east-1' │ 'rds' │ 'orders-before-upgrade' │ '2025-06-02' │ 'orders-db' │
│ 2 │ 'eu-west-1' │ 'rds-cluster' │ 'demo-aurora-share' │ '2025-11-20' │ 'demo-aurora' │
└─────────┴─────────────┴───────────────┴──────────────────────────┴──────────────┴─────────────────────────┘
3 public snapshots found.
IDs are placeholders. The EBS snapshot in us-east-1 is still public despite the block, because block-new-sharing leaves existing public snapshots alone. Fix the findings first, then switch the Region to block-all-sharing.
What should you do after finding a public snapshot?
- Make it privateRun the script with
--make-private --apply, or removeallin the console. - Assume it was copiedYou can’t tell from the snapshot who restored it while it was public. Treat any credentials, keys or customer data on that disk or database as exposed, and rotate them.
- Find out how it happenedSearch CloudTrail for
ModifySnapshotAttributeandModifyDBSnapshotAttributecalls to see who shared it and when. - Block it for the futureEnable block public access for EBS snapshots in every Region you use, and remove the modify actions from roles that don’t need them.
- Delete what you don’t needOld manual snapshots are also a cost. The example to find and delete old RDS manual snapshots handles the RDS side, and the one to clean up old AMIs and EBS snapshots the EC2 side. Snapshots whose source volume no longer exists show up when you find orphaned EBS snapshots whose volume is gone.
Troubleshooting
AccessDeniedonDescribeDBSnapshotAttributes.ReadOnlyAccessincludes it, but a custom audit policy often misses the attribute calls. The guide to troubleshoot AWS IAM access denied errors step by step covers SCPs and boundaries too.- Slow on accounts with many RDS snapshots. The script makes one attribute call per manual snapshot. Pass
--regions, or raisemaxAttemptsif you see throttling. enable-snapshot-block-public-accessfails with a declarative policy message. When AWS Organizations manages the setting through a declarative policy, you can’t change it inside the account; ask whoever manages the organization.- A snapshot is still restorable by a partner. That’s explicit sharing with an account ID, which this script deliberately leaves alone.
Ask ChatWithCloud instead
From a read-only profile, ask ChatWithCloud “Do I have any public EBS or RDS snapshots in eu-west-1?”. It writes AWS SDK for JavaScript v2 code, runs it locally with your profile and explains the result; see how ChatWithCloud answers questions about your AWS account. It works in one Region per session and runs changes without asking first, so use a profile without modify permissions for questions like this. The ChatWithCloud security page lists what is sent for processing, which includes snapshot IDs from the results.
Frequently asked questions
How do I find public EBS snapshots with the AWS CLI?
Run aws ec2 describe-snapshots --owner-ids self --restorable-by-user-ids all in each Region. For RDS, run aws rds describe-db-snapshot-attributes --db-snapshot-identifier NAME and look for all.
Can an encrypted snapshot be public?
No. Encrypted EBS snapshots can’t be made public, and encrypted RDS snapshots can only be shared with specific accounts.
Does block public access for EBS snapshots cost anything?
No. AWS states there is no additional charge. It is set per Region.
Does making a snapshot private break sharing with other accounts?
No. Removing all leaves explicitly listed account IDs in place.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud