Find KMS Keys Without Automatic Rotation and Enable It

A group of old brass keys lying on a wooden table

Photo by Jason D on Unsplash

To enable KMS key rotation where it’s missing, list your customer managed keys with ListKeys and DescribeKey, keep the enabled symmetric encryption keys whose key material KMS generated, and call GetKeyRotationStatus. For each key where KeyRotationEnabled is false, call EnableKeyRotation; the optional RotationPeriodInDays takes 90 to 2,560 days and defaults to 365.

Automatic rotation is off by default on the KMS keys you create, and many compliance checklists ask for it. Turning it on is one API call per key, but finding the keys that need it isn’t: some keys can’t rotate automatically at all, multi-Region replicas take the setting from their primary, and disabled keys refuse the change. This example is for engineers who want to enable KMS key rotation across every Region without guessing which keys qualify.

The TypeScript script uses the AWS SDK for JavaScript v3. It reports by default and changes a key only when you pass --apply. It complements the script to find unused customer managed KMS keys: retire the keys nobody uses first, then turn on rotation for the ones you keep.

What does KMS key rotation change?

Rotation gives a KMS key new cryptographic material and keeps every earlier version. KMS encrypts with the current material and automatically decrypts with whichever version encrypted the ciphertext, so nothing about how you use the key changes:

  • Same key. The key ID, ARN, aliases, key policy and grants stay the same. Applications and AWS services need no code or configuration change.
  • No re-encryption. Existing ciphertext stays as it is. Rotation also doesn’t rotate data keys the KMS key generated earlier, so it won’t fix a leaked data key.
  • Visible events. Each rotation writes a RotateKey event to CloudTrail and a KMS CMK Rotation event to EventBridge.

AWS managed keys (the aws/… aliases) rotate every year on their own and you can’t change that, so the script ignores them. For how long a key should stay in use, NIST SP 800-57 Part 1 Rev. 5 recommends setting a cryptoperiod for each key; the rotation period is where you encode yours.

Which KMS keys can’t rotate automatically?

Key Automatic rotation What to do instead
Symmetric encryption, AWS_KMS origin Yes Enable it; the script does this with --apply.
Imported key material (EXTERNAL) No On-demand rotation after importing new material, or manual rotation.
Asymmetric and HMAC keys No Manual rotation: create a new key and move the alias.
Keys in a custom key store No Manual rotation.
Multi-Region replica Inherited Set it on the primary key; KMS copies the setting.
Disabled key Can’t change Enable the key first, or leave it if it’s on its way out.

Keys pending deletion always report rotation as false, so the script skips them.

What does rotation cost?

From the AWS KMS pricing page and the AWS Price List for US East (N. Virginia), checked September 2026:

Item Price
Customer managed key $1 per month, prorated hourly
First and second rotation +$1 per month each, prorated hourly
Third and later rotations Not billed
Symmetric API requests $0.03 per 10,000 after 20,000 free per month

So a key with yearly rotation costs $1 a month in year one, $2 in year two and $3 from year three on. Turning on rotation for 20 keys adds $20 a month after the first rotation and $40 a month after the second: $480 a year at steady state. A shorter period reaches that cap sooner but costs no more after it.

What does the script do?

  1. Lists RegionsDescribeRegions, or the Regions you pass with --regions=.
  2. Maps aliasespaginateListAliases, so each key shows a readable name.
  3. Classifies every keypaginateListKeys, then DescribeKey: skips AWS managed keys and keys pending deletion, and marks keys that can’t rotate with the reason.
  4. Reads rotation statusGetKeyRotationStatus returns KeyRotationEnabled, RotationPeriodInDays and NextRotationDate.
  5. Enables rotation on requestWith --apply, EnableKeyRotation with --period days (default 365) for every enabled key marked OFF.

Prerequisites

Which IAM permissions does it need?

Replace the account ID. The last statement is for --apply only; leave it out of an audit role.

kms-rotation-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListRegionsKeysAliases",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeRegions",
        "kms:ListKeys",
        "kms:ListAliases"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ReadKeys",
      "Effect": "Allow",
      "Action": [
        "kms:DescribeKey",
        "kms:GetKeyRotationStatus"
      ],
      "Resource": "arn:aws:kms:*:111122223333:key/*"
    },
    {
      "Sid": "EnableRotationApplyOnly",
      "Effect": "Allow",
      "Action": "kms:EnableKeyRotation",
      "Resource": "arn:aws:kms:*:111122223333:key/*"
    }
  ]
}

KMS checks the key policy too. IAM permissions work only for keys whose policy delegates to the account (the default key policy does). Keys with a custom policy may return AccessDeniedException, which shows up as error in the report. To lock down who may choose the period, the kms:RotationPeriodInDays condition key limits the values allowed in EnableKeyRotation.

The script to enable KMS key rotation

find-kms-keys-without-rotation.ts

// find-kms-keys-without-rotation.ts
// Lists customer managed KMS keys in each Region, reports which ones can rotate automatically and whether
// rotation is on, and explains why the others can't. Dry run by default: with --apply it calls
// EnableKeyRotation on enabled, rotatable keys whose rotation is off.
// Usage: npx tsx find-kms-keys-without-rotation.ts [--regions=us-east-1,eu-west-1] [--period=365] [--apply]
import { EC2Client, DescribeRegionsCommand } from "@aws-sdk/client-ec2";
import {
  KMSClient,
  paginateListKeys,
  paginateListAliases,
  DescribeKeyCommand,
  GetKeyRotationStatusCommand,
  EnableKeyRotationCommand,
  type KeyMetadata,
} from "@aws-sdk/client-kms";

const args = process.argv.slice(2);
const apply = args.includes("--apply");
const regionArg = args.find((a) => a.startsWith("--regions="))?.split("=")[1]?.split(",").map((s) => s.trim()).filter(Boolean);
const period = Number(args.find((a) => a.startsWith("--period="))?.split("=")[1] ?? "365");
if (!Number.isInteger(period) || period < 90 || period > 2560) {
  console.error("--period must be a whole number of days from 90 to 2560");
  process.exit(1);
}

type Status = "OFF" | "on" | "can't rotate" | "disabled" | "replica" | "error";
interface Row {
  Region: string;
  KeyId: string;
  Alias: string;
  Status: Status;
  PeriodDays: number | string;
  NextRotation: string;
  Note: string;
}

async function listRegions(): Promise<string[]> {
  if (regionArg) return regionArg;
  const out = await new EC2Client({}).send(new DescribeRegionsCommand({}));
  return (out.Regions ?? []).map((r) => r.RegionName ?? "").filter(Boolean).sort();
}

// Automatic rotation works only for symmetric encryption keys with KMS-generated key material
// outside custom key stores. Returns the reason a key can't rotate, or undefined if it can.
function cannotRotate(m: KeyMetadata): string | undefined {
  if (m.KeySpec !== "SYMMETRIC_DEFAULT") return `${m.KeySpec ?? "unknown"} key`;
  if (m.Origin === "EXTERNAL") return "imported key material";
  if (m.CustomKeyStoreId) return "custom key store";
  return undefined;
}

async function scanRegion(region: string): Promise<Row[]> {
  const kms = new KMSClient({ region });
  const aliases = new Map<string, string>();
  for await (const page of paginateListAliases({ client: kms }, {})) {
    for (const a of page.Aliases ?? []) {
      if (a.TargetKeyId && a.AliasName && !aliases.has(a.TargetKeyId)) aliases.set(a.TargetKeyId, a.AliasName);
    }
  }

  const rows: Row[] = [];
  for await (const page of paginateListKeys({ client: kms }, {})) {
    for (const k of page.Keys ?? []) {
      if (!k.KeyId) continue;
      const base = { Region: region, KeyId: k.KeyId, Alias: aliases.get(k.KeyId) ?? "-", PeriodDays: "-", NextRotation: "-" };
      try {
        const m = (await kms.send(new DescribeKeyCommand({ KeyId: k.KeyId }))).KeyMetadata;
        if (!m || m.KeyManager !== "CUSTOMER") continue; // AWS managed keys rotate every year on their own
        if (m.KeyState === "PendingDeletion" || m.KeyState === "PendingReplicaDeletion") continue;
        const reason = cannotRotate(m);
        if (reason) {
          rows.push({ ...base, Status: "can't rotate", Note: reason });
          continue;
        }
        if (m.MultiRegionConfiguration?.MultiRegionKeyType === "REPLICA") {
          const primary = m.MultiRegionConfiguration.PrimaryKey?.Region ?? "?";
          rows.push({ ...base, Status: "replica", Note: `set rotation on the primary key in ${primary}` });
          continue;
        }
        const r = await kms.send(new GetKeyRotationStatusCommand({ KeyId: k.KeyId }));
        const enabled = m.KeyState === "Enabled";
        rows.push({
          ...base,
          Status: r.KeyRotationEnabled ? "on" : enabled ? "OFF" : "disabled",
          PeriodDays: r.RotationPeriodInDays ?? "-",
          NextRotation: r.NextRotationDate ? r.NextRotationDate.toISOString().slice(0, 10) : "-",
          Note: enabled ? "" : `key state ${m.KeyState}; enable the key first`,
        });
      } catch (err) {
        rows.push({ ...base, Status: "error", Note: err instanceof Error ? err.name : String(err) });
      }
    }
  }

  if (apply) {
    for (const row of rows.filter((r) => r.Status === "OFF")) {
      try {
        await kms.send(new EnableKeyRotationCommand({ KeyId: row.KeyId, RotationPeriodInDays: period }));
        row.Status = "on";
        row.PeriodDays = period;
        row.Note = "rotation enabled now";
      } catch (err) {
        row.Note = `enable failed: ${err instanceof Error ? err.name : String(err)}`;
      }
    }
  }
  return rows;
}

async function main(): Promise<void> {
  const rows: Row[] = [];
  for (const region of await listRegions()) {
    try {
      rows.push(...(await scanRegion(region)));
    } catch (err) {
      console.error(`${region}: ${err instanceof Error ? err.name : String(err)}`);
    }
  }
  const order: Record<Status, number> = { OFF: 0, disabled: 1, error: 2, replica: 3, "can't rotate": 4, on: 5 };
  rows.sort((a, b) => order[a.Status] - order[b.Status] || a.Region.localeCompare(b.Region));
  console.table(rows);

  const off = rows.filter((r) => r.Status === "OFF").length;
  const on = rows.filter((r) => r.Status === "on").length;
  console.log(`${rows.length} customer managed key(s): ${on} rotating, ${off} rotatable but OFF.`);
  if (!apply && off) {
    console.log(`Dry run. Re-run with --apply to enable rotation every ${period} days on the ${off} OFF key(s).`);
    process.exitCode = 2;
  }
}

main().catch((err) => {
  console.error(err instanceof Error ? `${err.name}: ${err.message}` : err);
  process.exit(1);
});

How do you run it?

Terminal

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

# Report every enabled Region (dry run)
AWS_PROFILE=security-audit npx tsx find-kms-keys-without-rotation.ts

# Turn rotation on, every 180 days, in two Regions
AWS_PROFILE=kms-admin npx tsx find-kms-keys-without-rotation.ts --regions=us-east-1,eu-west-1 --period=180 --apply

The dry run exits with code 2 when any rotatable key is OFF, which makes it a simple CI or scheduled check. The script calls KMS once or twice per key; for hundreds of keys, raise maxAttempts as shown in the guide to configure retries and timeouts in AWS SDK for JavaScript v3.

Sample output

Output

┌─────────┬─────────────┬────────────────────────────────────────┬─────────────────────┬────────────────┬────────────┬──────────────┬────────────────────────────────────────────────┐
│ (index) │ Region      │ KeyId                                  │ Alias               │ Status         │ PeriodDays │ NextRotation │ Note                                           │
├─────────┼─────────────┼────────────────────────────────────────┼─────────────────────┼────────────────┼────────────┼──────────────┼────────────────────────────────────────────────┤
│ 0       │ 'eu-west-1' │ '0b1f9e2a-7c44-4d5e-9a61-3f2c8d7e1a90' │ 'alias/orders-db'   │ 'OFF'          │ '-'        │ '-'          │ ''                                             │
│ 1       │ 'us-east-1' │ '5d2c7a10-8e3b-4f6a-b1c9-2e7d4a8f6b35' │ 'alias/app-secrets' │ 'OFF'          │ '-'        │ '-'          │ ''                                             │
│ 2       │ 'us-east-1' │ 'e7a3c1d9-2b5f-4c8e-a6d0-9f1b3e5c7a24' │ 'alias/old-etl'     │ 'disabled'     │ '-'        │ '-'          │ 'key state Disabled; enable the key first'     │
│ 3       │ 'us-west-2' │ 'mrk-4c1e8a7b2d9f4e6c8a1b3d5f7e9c2a4b' │ 'alias/global-data' │ 'replica'      │ '-'        │ '-'          │ 'set rotation on the primary key in us-east-1' │
│ 4       │ 'us-east-1' │ '9c8e7d6f-5a4b-4c3d-8e2f-1a0b9c8d7e6f' │ 'alias/jwt-signing' │ "can't rotate" │ '-'        │ '-'          │ 'RSA_2048 key'                                 │
│ 5       │ 'us-east-1' │ '2a4c6e8f-1b3d-4f5a-9c7e-6d8f0a2c4e6b' │ 'alias/s3-logs'     │ 'on'           │ 365        │ '2027-02-11' │ ''                                             │
└─────────┴─────────────┴────────────────────────────────────────┴─────────────────────┴────────────────┴────────────┴──────────────┴────────────────────────────────────────────────┘
6 customer managed key(s): 1 rotating, 2 rotatable but OFF.
Dry run. Re-run with --apply to enable rotation every 365 days on the 2 OFF key(s).

The key IDs are illustrative. Two keys need rotation turned on. old-etl is disabled: check whether it still protects anything before re-enabling it, or schedule it for deletion. jwt-signing is an RSA key and has to be rotated by creating a new key and moving the alias.

Troubleshooting

  • DisabledException on EnableKeyRotation. The key was disabled between the report and the change. Re-run the report.
  • UnsupportedOperationException. The key is asymmetric, HMAC, imported or in a custom key store; the script should have marked it, so report the key spec if you see this.
  • AccessDeniedException for some keys only. Their key policy doesn’t delegate to IAM. Add the audit role to the key policy, or run the script as a principal the policy already names.
  • A Region is skipped. An SCP may deny KMS in unused Regions; the error name is printed on stderr.

Secrets Manager secrets are often the busiest users of customer managed keys, so find unused Secrets Manager secrets before deciding which keys to keep. Rotation is one of several encryption settings auditors ask about. The scripts to find unencrypted EBS volumes and turn on default encryption and to find RDS instances without automated backups or encryption cover the data those keys protect, and checking AWS Config is recording in every Region keeps a history of key configuration changes.

Ask ChatWithCloud instead

ChatWithCloud turns a plain-English question into AWS SDK for JavaScript v2 code, runs it locally with your profile and sends the JSON result to the AI model to write the answer. “Which customer managed KMS keys in eu-west-1 don’t have automatic rotation enabled?” runs the same checks. Changes run without a confirmation step, so ask “enable rotation” questions only from a profile you intend to write with, and explore from a read-only AWS profile connected to ChatWithCloud. The ChatWithCloud security model lists what leaves your machine, and the IAM policy generator for TypeScript SDK code drafts a policy if you extend the script.

Frequently asked questions

Does enabling KMS key rotation break existing encrypted data?

No. KMS keeps every earlier version of the key material and picks the right one to decrypt. The key ID and ARN don’t change, so no application change is needed.

How often does AWS KMS rotate a key?

Every 365 days by default once you enable rotation. You can set RotationPeriodInDays anywhere from 90 to 2,560 days. AWS managed keys rotate every year and can’t be changed.

Can I rotate a KMS key right now?

Yes, with on-demand rotation (RotateKeyOnDemand) on symmetric encryption keys, including keys with imported material. It doesn’t change the automatic schedule.

How do I rotate an asymmetric KMS key?

Manually: create a new key, point the alias and your applications at it, and keep the old key until nothing needs it to decrypt or verify.

Related guides

Ask your AWS account in plain English

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

npx chatwithcloud