Skip to content

RDS / Aurora encryption with customer-managed KMS keys — the gotcha you cannot reverse

Encryption at rest is set at create time and cannot be changed. Customer-managed KMS keys give you the audit trail and the ability to revoke. Key rotation is automatic but does not re-encrypt data. Cross-account snapshot sharing requires a precise key policy. The actual setup and the trust block that has to align.

The compliance review asks “are your databases encrypted at rest?” and the answer is yes — RDS turns it on by default with the AWS-managed key aws/rds. The follow-up is “with a customer-managed key?” and the answer is no, because the original instance was created two years ago when nobody was thinking about KMS key custody. The next question is “can we move the existing data to a customer-managed key?” and the honest answer is “not in place.” Encryption status on RDS is set at instance creation and cannot be changed — there is no modify-db-instance that toggles it. The workaround is a snapshot, copy with a new KMS key, and restore. The right answer is to use a customer-managed key from the start.

What’s happening

RDS and Aurora encrypt the storage volume, automated backups, snapshots, and read replicas with a KMS key chosen at creation time. Three things to understand:

The encryption flag is immutable. StorageEncrypted: false cannot become true on a running instance. Same the other way — an encrypted instance cannot be turned unencrypted. The only path to change encryption status is snapshot → copy-with-different-key → restore-from-copy. This is a multi-hour operation for large databases and involves cutover downtime.

The KMS key is bound to the instance. Each encrypted RDS instance has a single KMS key associated. Read replicas, automated backups, and snapshots inherit it (with a wrinkle — see cross-region below). You can change the key by going through the same snapshot → copy → restore path, but not in-place.

KMS key rotation is automatic, but it does not re-encrypt data. When AWS-managed or customer-managed key auto-rotation is enabled, KMS generates a new key version every year. The new version is used for new encryptions. Existing ciphertext stays encrypted with the older version, which KMS keeps around to decrypt it. “Rotating the key” does not mean “all the data is re-encrypted with the new version.” That distinction matters for compliance frameworks that ask for evidence of re-keying.

The choice of customer-managed key (CMK) versus AWS-managed key (aws/rds) matters for three operational reasons:

  • Audit trail. CloudTrail records every Encrypt and Decrypt call against your CMK. The AWS-managed key calls also appear, but they are noisier and harder to filter.
  • Revocability. A CMK can be disabled or scheduled for deletion. Disabling the key makes the database unable to perform IO — the instance stops working. This is desirable in some incident-response scenarios (the legal team asks for “freeze the data”) and disastrous in others (someone accidentally disables the key). The AWS-managed key cannot be disabled by you.
  • Cross-account sharing. Sharing a snapshot to another AWS account requires a CMK with a key policy granting the other account’s access. The AWS-managed key cannot be shared across accounts — the snapshot stays in the source account.

CMKs cost a small monthly fee per key plus per-use API charges. The cost is negligible for production workloads; do not let it drive the decision.

The cross-region story has its own quirk. A snapshot encrypted with a source-region CMK cannot be copied to a different region as-is. The copy operation requires you to specify a destination-region CMK, and KMS re-encrypts the data envelope during the copy. The destination CMK can be a CMK in the destination region that grants the source account access (typical), or a multi-region CMK (newer AWS feature, the same logical key with replicas in multiple regions).

How to verify

Check the current encryption status:

aws rds describe-db-instances \
  --db-instance-identifier prod-db-1 \
  --query 'DBInstances[].[DBInstanceIdentifier,StorageEncrypted,KmsKeyId]' \
  --output table

If StorageEncrypted is false, the instance is not encrypted at rest. There is no in-place fix — see the migration plan below.

If StorageEncrypted is true and KmsKeyId ends in :key/aws/rds (or shows the alias alias/aws/rds), it is using the AWS-managed key. To check the key:

aws kms describe-key --key-id <KmsKeyId> \
  --query 'KeyMetadata.[KeyManager,Description,KeyState,Origin]' \
  --output table

KeyManager is AWS for AWS-managed and CUSTOMER for customer-managed.

Check the key’s rotation status:

aws kms get-key-rotation-status --key-id <KmsKeyId>

KeyRotationEnabled: true means annual auto-rotation is on. If the value is false, the key has not rotated automatically since creation. Enable it unless you have a specific reason not to:

aws kms enable-key-rotation --key-id <KmsKeyId>

For cross-account snapshot sharing, look at the key policy:

aws kms get-key-policy --key-id <KmsKeyId> --policy-name default \
  --query 'Policy' --output text | jq .

A key policy that allows another account to use the key has a statement like:

{
  "Sid": "AllowExternalAccountToUseKey",
  "Effect": "Allow",
  "Principal": {"AWS": "arn:aws:iam::OTHER-ACCOUNT-ID:root"},
  "Action": [
    "kms:Decrypt",
    "kms:CreateGrant",
    "kms:DescribeKey"
  ],
  "Resource": "*"
}

Without that statement, snapshot sharing to the other account fails with a vague “access denied” error.

The fix

Three scenarios, each with its own runbook.

Scenario 1: New instance with CMK from day one

The right path. Create the CMK first:

aws kms create-key \
  --description "RDS encryption key for prod-db-1" \
  --tags TagKey=Project,TagValue=production-platform

# Output includes KeyId; save it.

aws kms create-alias \
  --alias-name alias/rds-prod-db-1 \
  --target-key-id <KeyId>

aws kms enable-key-rotation --key-id <KeyId>

Use a key policy that allows RDS to use the key and your operations role to manage it:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnableRootPermissions",
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::123456789012:root"},
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "AllowRDSToUseTheKey",
      "Effect": "Allow",
      "Principal": {"Service": "rds.amazonaws.com"},
      "Action": [
        "kms:Decrypt",
        "kms:DescribeKey",
        "kms:Encrypt",
        "kms:ReEncrypt*",
        "kms:GenerateDataKey*",
        "kms:CreateGrant"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "rds.us-east-1.amazonaws.com",
          "kms:CallerAccount": "123456789012"
        }
      }
    }
  ]
}

The kms:ViaService condition restricts the key to being used by RDS in that region, which prevents cross-service misuse. The kms:CallerAccount condition belt-and-suspenders the account.

Create the database with the CMK:

aws rds create-db-instance \
  --db-instance-identifier prod-db-1 \
  --engine postgres \
  --engine-version 15.5 \
  --db-instance-class db.r6g.xlarge \
  --master-username postgres \
  --master-user-password REDACTED \
  --allocated-storage 100 \
  --storage-encrypted \
  --kms-key-id alias/rds-prod-db-1 \
  --vpc-security-group-ids sg-db \
  --db-subnet-group-name prod-db-subnets \
  --multi-az \
  --backup-retention-period 14 \
  --no-publicly-accessible

Scenario 2: Migrate existing unencrypted instance to CMK

The instance is StorageEncrypted: false. Direct conversion is not possible. The migration:

# 1. Take a final snapshot of the unencrypted instance.
aws rds create-db-snapshot \
  --db-snapshot-identifier prod-db-1-pre-encryption \
  --db-instance-identifier prod-db-1

# 2. Copy the snapshot, applying encryption with the new CMK.
aws rds copy-db-snapshot \
  --source-db-snapshot-identifier prod-db-1-pre-encryption \
  --target-db-snapshot-identifier prod-db-1-encrypted \
  --kms-key-id alias/rds-prod-db-1

# 3. Restore the encrypted snapshot to a new instance.
aws rds restore-db-snapshot \
  --db-instance-identifier prod-db-1-new \
  --db-snapshot-identifier prod-db-1-encrypted \
  --db-instance-class db.r6g.xlarge \
  --multi-az \
  --vpc-security-group-ids sg-db \
  --db-subnet-group-name prod-db-subnets

# 4. After verification, cutover the application to the new instance,
#    rename the old one out of the way, rename the new one into place.

The cutover requires application downtime — the snapshot is point-in-time-of-snapshot, so any writes after the snapshot need to be replayed (using Postgres logical replication or a brief lock window). For a small database, the whole cutover is 5–15 minutes; for a multi-TB database, a logical replication catch-up window is required.

Scenario 3: Cross-account snapshot share with CMK

The other account needs a copy of the snapshot for DR or compliance purposes. The steps:

# 1. Update the source key policy to allow the destination account.
aws kms put-key-policy \
  --key-id alias/rds-prod-db-1 \
  --policy-name default \
  --policy file://updated-key-policy.json

# Where updated-key-policy.json adds the destination-account statement:
# {
#   "Sid": "AllowDRAccount",
#   "Effect": "Allow",
#   "Principal": {"AWS": "arn:aws:iam::DR-ACCOUNT:root"},
#   "Action": ["kms:Decrypt","kms:CreateGrant","kms:DescribeKey"],
#   "Resource": "*"
# }

# 2. Share the snapshot.
aws rds modify-db-snapshot-attribute \
  --db-snapshot-identifier prod-db-1-encrypted-snap \
  --attribute-name restore \
  --values-to-add DR-ACCOUNT-ID

# 3. In the DR account, copy the snapshot (re-encrypting with a DR-account CMK).
# This step is run as the DR account.
aws rds copy-db-snapshot \
  --source-db-snapshot-identifier arn:aws:rds:us-east-1:SOURCE-ACCOUNT:snapshot:prod-db-1-encrypted-snap \
  --target-db-snapshot-identifier prod-db-1-dr-copy \
  --kms-key-id alias/rds-dr-key

Three things that bite during encryption work:

The “kms:GrantIsForAWSResource” condition. Some RDS operations require KMS grants. The key policy must allow kms:CreateGrant with the kms:GrantIsForAWSResource: true condition. Without it, automated snapshot copy across regions fails silently.

Replica encryption keys differ. A cross-region read replica or cross-region snapshot uses a destination-region key. The source-region CMK does not work in another region. Build the destination-region CMK first, reference it explicitly.

Disabling a key in use is a real outage. A CMK that an RDS instance depends on, if disabled or scheduled for deletion, causes the instance to lose access to its storage within minutes. The database goes into a state where reads and writes fail. CloudTrail catches the disable event, but recovery requires re-enabling the key. Keep CMK deletion authority tightly scoped — the operations team should not be one click away from killing a database.

The encryption-and-key strategy is one of the few RDS decisions that is genuinely difficult to reverse cheaply. Pick the CMK posture at instance creation, document the key policy, and put guardrails on key deletion. Re-encrypting after the fact is possible but operationally expensive.


Stack Harbor designs and operates production RDS / Aurora topologies as part of Managed AWS and the ongoing Managed Cloud Ops layer. If your database posture is “Multi-AZ checkbox + hope,” start with an infrastructure consult.