Photo by William Warby on Unsplash
To find unencrypted EBS volumes, call DescribeVolumes in each Region with the filter encrypted=false, and call GetEbsEncryptionByDefault to see whether new volumes are protected. Turning on encryption by default with EnableEbsEncryptionByDefault covers new volumes only; existing ones must be encrypted through a snapshot and a new volume.
This example is for engineers who need to find unencrypted EBS volumes across an AWS account, usually because an audit or a CIS-style checklist asked, and then stop new ones from appearing. The script uses AWS SDK for JavaScript v3, scans every enabled Region, and changes nothing unless you pass two explicit flags.
It pairs with the example to find and tag unattached EBS volumes: that one hunts for waste, this one for exposure. Both are part of our collection of AWS SDK v3 practical examples.
What does the script check?
Two things per Region, because EBS encryption is a Regional matter:
| Check | API call | What it tells you |
|---|---|---|
| Unencrypted volumes | DescribeVolumes with filter encrypted=false |
Every volume, attached or not, whose data isn’t encrypted at rest |
| Encryption by default | GetEbsEncryptionByDefault |
Whether new volumes and snapshot copies in the Region are encrypted automatically |
| Default KMS key | GetEbsDefaultKmsKeyId |
Which key new volumes get: the AWS managed aws/ebs key or a customer managed key |
Three facts from the AWS documentation shape the fix. Encryption by default is a Region-specific setting. It has no effect on existing EBS volumes or snapshots. And you can’t directly encrypt an existing unencrypted volume: you snapshot it and create a new, encrypted volume from that snapshot. The Amazon EBS guide to encryption by default covers the console and CLI versions of the same steps.
Prerequisites
- Node.js 20 or later, npm and
tsx. - The
@aws-sdk/client-ec2package. - A profile for the account you’re auditing. The Region list comes from
DescribeRegions, which returns only the Regions enabled for your account; pass--regionsto limit the scan.
Which IAM permissions does it need?
The report needs four read actions. ec2:EnableEbsEncryptionByDefault is only for --enable-default --apply; leave that statement out of an audit-only role. None of these actions support resource-level permissions, so the resource is *.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadEbsEncryptionState",
"Effect": "Allow",
"Action": [
"ec2:DescribeRegions",
"ec2:DescribeVolumes",
"ec2:GetEbsEncryptionByDefault",
"ec2:GetEbsDefaultKmsKeyId"
],
"Resource": "*"
},
{
"Sid": "OptionalEnableDefaultEncryption",
"Effect": "Allow",
"Action": "ec2:EnableEbsEncryptionByDefault",
"Resource": "*"
}
]
}
If you adapt the script, the IAM policy generator for TypeScript code drafts a policy from your version, and the checklist to review a generated IAM policy for least privilege helps you trim it.
The script to find unencrypted EBS volumes
// find-unencrypted-ebs-volumes.ts
// Lists unencrypted EBS volumes in every enabled Region and shows whether
// EBS encryption by default is on. Read-only by default.
// With --enable-default it shows the Regions where it would turn on encryption
// by default (dry run); add --apply to make the change. Existing volumes are never modified.
// Usage: npx tsx find-unencrypted-ebs-volumes.ts [--regions us-east-1,eu-west-1] [--enable-default [--apply]]
import {
EC2Client,
DescribeRegionsCommand,
GetEbsEncryptionByDefaultCommand,
GetEbsDefaultKmsKeyIdCommand,
EnableEbsEncryptionByDefaultCommand,
paginateDescribeVolumes,
} from "@aws-sdk/client-ec2";
const enableDefault = process.argv.includes("--enable-default");
const apply = process.argv.includes("--apply");
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;
}
type VolumeRow = {
region: string;
volumeId: string;
sizeGiB: number;
type: string;
state: string;
instance: string;
name: string;
};
type RegionRow = {
region: string;
defaultEncryption: string;
defaultKey: string;
unencrypted: number;
};
async function listRegions(): Promise<string[]> {
const explicit = regionsArg();
if (explicit) return explicit;
const ec2 = new EC2Client({ region: process.env.AWS_REGION ?? "us-east-1" });
// Without AllRegions, DescribeRegions returns only the Regions enabled for the account.
const { Regions } = await ec2.send(new DescribeRegionsCommand({}));
return (Regions ?? []).map((r) => r.RegionName).filter((r): r is string => Boolean(r)).sort();
}
async function main(): Promise<void> {
const volumes: VolumeRow[] = [];
const regions: RegionRow[] = [];
for (const region of await listRegions()) {
const ec2 = new EC2Client({ region, maxAttempts: 5 });
const { EbsEncryptionByDefault } = await ec2.send(new GetEbsEncryptionByDefaultCommand({}));
const { KmsKeyId } = await ec2.send(new GetEbsDefaultKmsKeyIdCommand({}));
let count = 0;
const pages = paginateDescribeVolumes(
{ client: ec2 },
{ Filters: [{ Name: "encrypted", Values: ["false"] }] },
);
for await (const page of pages) {
for (const v of page.Volumes ?? []) {
count++;
volumes.push({
region,
volumeId: v.VolumeId ?? "?",
sizeGiB: v.Size ?? 0,
type: v.VolumeType ?? "?",
state: v.State ?? "?",
instance: v.Attachments?.[0]?.InstanceId ?? "-",
name: v.Tags?.find((t) => t.Key === "Name")?.Value ?? "",
});
}
}
regions.push({
region,
defaultEncryption: EbsEncryptionByDefault ? "on" : "OFF",
defaultKey: KmsKeyId ?? "-",
unencrypted: count,
});
}
console.table(regions);
console.table(volumes);
const totalGiB = volumes.reduce((sum, v) => sum + v.sizeGiB, 0);
console.log(`${volumes.length} unencrypted volumes (${totalGiB} GiB) in ${regions.length} Regions.`);
if (!enableDefault) return;
const targets = regions.filter((r) => r.defaultEncryption === "OFF");
console.log(`\n${apply ? "Enabling" : "Dry run: would enable"} encryption by default in ${targets.length} Regions:`);
for (const r of targets) {
console.log(` ${r.region}`);
if (!apply) continue;
const ec2 = new EC2Client({ region: r.region });
const out = await ec2.send(new EnableEbsEncryptionByDefaultCommand({}));
console.log(` EbsEncryptionByDefault=${String(out.EbsEncryptionByDefault)}`);
}
if (!apply && targets.length > 0) console.log("Re-run with --enable-default --apply to make the change.");
}
main().catch((err: unknown) => {
console.error(err);
process.exit(1);
});
The safety rails: nothing changes without --enable-default --apply; the only write call is EnableEbsEncryptionByDefault, which affects future volumes; and existing volumes are never touched, because encrypting them means replacing them.
How do you run it?
npm install @aws-sdk/client-ec2
npm install --save-dev tsx typescript
# Report: every enabled Region
AWS_PROFILE=security-audit npx tsx find-unencrypted-ebs-volumes.ts
# Only the Regions you use
AWS_PROFILE=security-audit npx tsx find-unencrypted-ebs-volumes.ts --regions us-east-1,eu-west-1
# Dry run: which Regions would get encryption by default
AWS_PROFILE=ops-admin npx tsx find-unencrypted-ebs-volumes.ts --enable-default
# Make the change
AWS_PROFILE=ops-admin npx tsx find-unencrypted-ebs-volumes.ts --enable-default --apply
Sample output
┌─────────┬─────────────┬───────────────────┬─────────────────┬─────────────┐
│ (index) │ region │ defaultEncryption │ defaultKey │ unencrypted │
├─────────┼─────────────┼───────────────────┼─────────────────┼─────────────┤
│ 0 │ 'eu-west-1' │ 'OFF' │ 'alias/aws/ebs' │ 1 │
│ 1 │ 'us-east-1' │ 'OFF' │ 'alias/aws/ebs' │ 2 │
│ 2 │ 'us-west-2' │ 'on' │ 'alias/aws/ebs' │ 0 │
└─────────┴─────────────┴───────────────────┴─────────────────┴─────────────┘
┌─────────┬─────────────┬─────────────────────────┬─────────┬───────┬─────────────┬───────────────────────┬──────────────┐
│ (index) │ region │ volumeId │ sizeGiB │ type │ state │ instance │ name │
├─────────┼─────────────┼─────────────────────────┼─────────┼───────┼─────────────┼───────────────────────┼──────────────┤
│ 0 │ 'eu-west-1' │ 'vol-0a1b2c3d4e5f60718' │ 100 │ 'gp2' │ 'available' │ '-' │ 'old-export' │
│ 1 │ 'us-east-1' │ 'vol-049df61146c4d7901' │ 30 │ 'gp3' │ 'in-use' │ 'i-0123456789abcdef0' │ 'web-1-root' │
│ 2 │ 'us-east-1' │ 'vol-0f9e8d7c6b5a43210' │ 500 │ 'gp3' │ 'in-use' │ 'i-0123456789abcdef0' │ 'web-1-data' │
└─────────┴─────────────┴─────────────────────────┴─────────┴───────┴─────────────┴───────────────────────┴──────────────┘
3 unencrypted volumes (630 GiB) in 3 Regions.
IDs are placeholders. Read it in two passes. First, the Region table: every OFF row can keep producing unencrypted volumes, so fix those first with --enable-default --apply. Second, the volume table: the available volume in eu-west-1 may not need encrypting at all. Check whether anyone still needs it before you spend time on a copy, and while you’re there, see if old gp2 volumes should also convert from gp2 to gp3 with SDK v3.
How do you encrypt an existing EBS volume?
There is no “encrypt in place” call. Replace the volume:
- Turn on encryption by default firstWith it on, a volume created from an unencrypted snapshot is always encrypted, so you don’t need to pass encryption parameters in each request.
- Snapshot the volume
CreateSnapshoton the unencrypted volume. For a consistent copy of a busy data volume, pause writes or stop the instance first. - Create an encrypted volume from the snapshot
CreateVolumewith the snapshot ID in the same Availability Zone as the instance. It comes out encrypted with the Region’s default key, or the key you name. - Swap the volumesStop the instance, detach the old volume, attach the new one at the same device name, start the instance and check the application. Root volumes require the instance to be stopped for the swap.
- Keep the old volume for a while, then delete itOnce you’re satisfied, delete the old volume and the interim snapshot. The example to clean up AMIs and snapshots older than 30 days catches the leftovers.
Two caveats. You can’t change the KMS key of an existing encrypted volume or snapshot, but you can pick a different key when you copy a snapshot. And snapshots encrypted with the AWS managed aws/ebs key can’t be shared with other accounts, so if you copy AMIs or snapshots across accounts, set a customer managed key as the default before you start. If you already have several, the script to find unused customer managed KMS keys shows which ones are still in use. Whichever customer managed key becomes the default, the script to find KMS keys without automatic rotation makes sure it rotates.
Troubleshooting
UnauthorizedOperationonGetEbsDefaultKmsKeyId. Add the action to the policy, or remove that call if you don’t need the key column. To see which layer denies it, follow the steps to troubleshoot an AWS IAM access denied error.UnauthorizedOperationin only one or two Regions. A service control policy may deny those Regions. Rerun with--regionslisting only the Regions you use.- New instances still launch unencrypted volumes. Check the Region: the setting is per Region, and a launch in a Region with
OFFisn’t covered. - An instance fails to start after the swap. If the default key is a customer managed key, the role or user launching instances needs permission to use it, and the key policy must allow it.
Ask ChatWithCloud instead
From a read-only profile you can ask ChatWithCloud “Which EBS volumes are unencrypted, and is encryption by default on in us-east-1?”. It writes AWS SDK for JavaScript v2 code, runs it on your machine with your profile and summarizes the result; how ChatWithCloud runs AWS SDK code locally shows the loop. It uses one profile and Region per session, so a multi-Region sweep like this script is easier from code. It runs changes without a confirmation step, so don’t ask it to enable anything from a profile that allows ec2:EnableEbsEncryptionByDefault unless you mean it. The walkthrough to analyze your AWS security posture with an AI CLI has more questions of this kind.
For the rest of a storage review, run the companion script to find public EBS and RDS snapshots, which catches data shared with every AWS account. Databases need the same review: the script to find unencrypted RDS instances and Aurora clusters checks encryption at rest in every Region. Shared file storage has the same create-time catch, and the script to find unencrypted EFS file systems lists the ones to migrate.
Frequently asked questions
How do I find unencrypted EBS volumes with the AWS CLI?
Run aws ec2 describe-volumes --filters Name=encrypted,Values=false in each Region, and aws ec2 get-ebs-encryption-by-default to check the Region setting.
Does enabling EBS encryption by default encrypt existing volumes?
No. It applies to new volumes and snapshot copies only. Existing volumes need the snapshot-and-replace steps above.
Is EBS encryption by default a global setting?
No, it’s set per Region. Enable it in every Region you use, including ones you rarely touch.
Can I turn encryption off for one volume after enabling the default?
No. Once it’s on in a Region, you can’t create unencrypted volumes or snapshot copies there.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud