AWS KMS Envelope Encryption and Data Key Caching Patterns - Design Decisions for the AWS Encryption SDK and Multi-Region Keys

First Published:
Last Updated:

Every application that protects data on AWS eventually reaches the same fork in the road. You have an AWS KMS key. You have data. How does one actually encrypt the other?

The naive answer — call Encrypt, get ciphertext back — works for exactly one class of payload and quietly falls apart for everything else. AWS KMS will not encrypt more than 4,096 bytes under a symmetric encryption KMS key, every byte you do send crosses the network into a service with a shared, per-Region request rate quota, and the plaintext leaves your process to be handled by something else. The pattern that scales instead is envelope encryption: ask KMS for a short-lived data key, encrypt your payload locally with it, throw the plaintext data key away, and store the KMS-encrypted copy of that data key next to the ciphertext.

That much is well documented. What is not well documented is the set of design decisions that sit on top of it. Should the encryption happen in your code or in the service's storage layer? If you use the AWS Encryption SDK, which keyring, which algorithm suite, which commitment policy — and what breaks when you upgrade? Should you cache data keys, and if so, what exactly are you trading away when you do? What happens to your encryption context when someone enables S3 Bucket Keys? Which properties of a multi-Region key are actually synchronized, and which ones will silently drift between Regions?

This guide answers those questions. It is a design guide, not a service tour.

Scope and honesty notes. This article covers envelope encryption design on AWS, the AWS Encryption SDK's configuration choices, data key caching, service-level optimizations such as S3 Bucket Keys, multi-Region and cross-account key design, auditing, and the failure modes that show up in production. Every factual claim below was verified against AWS's official documentation as of 2026-07-31; quotas and defaults change, so confirm current values for your account and Region before you rely on them. No AWS API calls were executed to produce this article and no measurements were taken — nothing here is presented as a benchmark. This article does not discuss pricing. Data key caching and S3 Bucket Keys are usually introduced as cost-reduction features; here they are evaluated purely on latency, throughput, request-rate quota headroom, and throttling resilience, because those are the properties that determine whether your architecture works. Every code fragment follows the patterns in AWS's documentation and is illustrative, not a tested deployment artifact. Operations that destroy access to data — scheduling key deletion, disabling a key, rewriting a key policy — are irreversible or near-irreversible and are flagged with warnings wherever they appear.

Several adjacent topics are deliberately delegated to existing articles rather than repeated here:

Table of Contents

  1. 1. Introduction
  2. 2. Why Envelope Encryption
  3. 3. The Data Key Lifecycle
  4. 4. Encryption Context
  5. 5. Client-Side versus Server-Side Encryption
  6. 6. AWS Encryption SDK Design Choices
  7. 7. Data Key Caching
  8. 8. Service-Level Optimizations
  9. 9. Multi-Region and Cross-Account Design
  10. 10. Key Policy and IAM Design
  11. 11. Auditing and Detection
  12. 12. Failure Modes and Anti-Patterns
  13. 13. Frequently Asked Questions
  14. 14. Summary
  15. 15. References

1. Introduction

The audience for this guide is the engineer who has to make an implementation decision this week: application developers, platform engineers, and security architects building on AWS who need to encrypt data and are choosing between "let the service do it" and "do it in my own code," and who need to know whether reusing a data key is acceptable in their design.

The guide is organized as a decision path. Sections 2 through 4 establish the mechanics you need to reason about anything else: why envelope encryption exists, how a data key lives and dies, and what the encryption context is actually for. Section 5 is the first real fork — client-side or server-side. Sections 6 and 7 go deep on the AWS Encryption SDK and on data key caching, which is where most of the genuinely difficult judgment calls live. Sections 8 through 11 cover the surrounding design: service-level optimizations, multi-Region and cross-account topology, key policy design, and audit. Section 12 collects the failure modes.

Three ideas run through all of it:
  • A data key is a blast radius. Every design decision in envelope encryption — how many data keys, how long each one lives, how many messages each one protects, whether they are shared across tenants — is a decision about how much data one compromised key exposes. Caching, bucket keys, and hierarchical keyrings are all, viewed correctly, decisions to widen that radius in exchange for fewer round trips.
  • Encryption context is the cheapest control you are not using. It is optional, it costs nothing structural, it binds ciphertext to its purpose, it becomes an IAM condition, and it becomes an audit dimension. Skipping it is the single most common avoidable mistake in the designs this guide covers.
  • Decryptability is a permanent obligation. The moment you write ciphertext, you have promised your future self that the wrapping key, the key policy, the Region, the account, the algorithm suite, and the library version will all still line up when you need to read it back. Most of the anti-patterns in Section 12 are broken versions of that promise.

2. Why Envelope Encryption

2.1 What direct encryption cannot do

AWS KMS exposes a straightforward Encrypt operation, and for small values it is the right tool. The constraint that ends the conversation for everything else is size. The maximum amount of data you can pass to Encrypt depends on the key type and algorithm:
* You can sort the table by clicking on the column name.
KMS key specEncryption algorithmMaximum plaintext size
Symmetric encryptionSYMMETRIC_DEFAULT4,096 bytes
RSA_2048RSAES_OAEP_SHA_1214 bytes
RSA_2048RSAES_OAEP_SHA_256190 bytes
RSA_3072RSAES_OAEP_SHA_1342 bytes
RSA_3072RSAES_OAEP_SHA_256318 bytes
RSA_4096RSAES_OAEP_SHA_1470 bytes
RSA_4096RSAES_OAEP_SHA_256446 bytes
SM2 (China Regions only)SM2PKE1,024 bytes

A 4 KB ceiling covers a password, a token, or a small structured record. It does not cover a document, an image, a database row set, a log batch, or a message payload.

Size is not the only limit. Direct encryption also means:
  • Every byte crosses a service boundary. The plaintext leaves your process, travels to a regional KMS endpoint, and comes back as ciphertext. That is a network round trip per operation, on the request path.
  • Every operation consumes shared request-rate quota. Encrypt, Decrypt, GenerateDataKey, GenerateDataKeyWithoutPlaintext, GenerateMac, GenerateRandom, ReEncrypt, and VerifyMac all draw on the same shared cryptographic operations request-rate quota, calculated per Region and per key type. The default is 10,000 requests per second in most Regions; 20,000 in US East (Ohio), Asia Pacific (Singapore), Asia Pacific (Sydney), Asia Pacific (Tokyo), Europe (Frankfurt), and Europe (London); and 100,000 in US East (N. Virginia), US West (Oregon), and Europe (Ireland). These quotas are adjustable, but they are shared across every client in the account — which means one chatty workload can throttle an unrelated one.
  • You cannot decrypt without KMS. That is usually a feature. It becomes an availability consideration when the decrypt path is on a hot request loop.

2.2 The envelope construction

Envelope encryption is the practice of encrypting plaintext data with a data key, and then encrypting that data key under another key. AWS KMS's own description of the construction is precise: a per-message key msgKey encrypts the message (ciphertext = Encrypt(msgKey, message)), a longer-term static key k encrypts the message key (encKey = Encrypt(k, msgKey)), and the pair (encKey, ciphertext) is packaged into a single envelope-encrypted message. A recipient with access to k opens the envelope by decrypting the key first and the message second.

On AWS, k is the KMS key. Its key material never leaves AWS KMS's FIPS 140-3 Security Level 3 validated hardware security modules in plaintext, which is the property the whole design rests on. msgKey is the data key — generated by KMS, handed to you in plaintext exactly once, and never stored by KMS.
Envelope encryption data flow with AWS KMS
Envelope encryption data flow with AWS KMS

2.3 What the construction buys you

  • Unbounded payload size. The 4 KB ceiling applies to what KMS encrypts, and KMS only ever encrypts a 256-bit data key. Your payload can be any size your local cipher can handle.
  • Local-speed bulk encryption. AES-GCM in your process is orders of magnitude closer to memory bandwidth than a network round trip. Only the key operation is remote.
  • Fewer KMS requests per byte. One GenerateDataKey call can protect an arbitrarily large object. This is the property every optimization later in this guide extends: S3 Bucket Keys, the caching cryptographic materials manager, and the Hierarchical keyring are all mechanisms for amortizing that one call over more data.
  • Safe storage of the wrapped key. Because the data key is itself encrypted, you can store it in the clear alongside the ciphertext — in object metadata, in a database column, in a message header — without a separate key store.
  • Independent re-wrapping. You can re-encrypt the data key under a different KMS key (ReEncrypt) without ever touching, moving, or decrypting the bulk ciphertext.

The cost is that the plaintext data key exists in your process's memory, however briefly. Section 3 is about making that window as small as it can be, and Section 7 is about what happens when you deliberately make it longer.

3. The Data Key Lifecycle

3.1 Generating a data key

GenerateDataKey returns a unique symmetric data key for use outside of KMS: a plaintext copy and a copy encrypted under the symmetric encryption KMS key you specify. The bytes in the plaintext key are random and are not related to the caller or to the KMS key.

Two parameter rules matter:
  • You must specify the length using either KeySpec or NumberOfBytes, but not both. For 128-bit and 256-bit data keys, use KeySpec (AES_256 is the normal choice).
  • You cannot use an asymmetric KMS key to generate data keys. Use DescribeKey if you are not sure what a key is.
aws kms generate-data-key \
  --key-id "alias/payments-records" \
  --key-spec AES_256 \
  --encryption-context "tenant=acme,record_type=invoice,env=prod"
The response contains three fields: Plaintext (base64 data key), CiphertextBlob (the encrypted data key), and KeyId. AWS's recommended pattern is explicit — use GenerateDataKey to get the key, use the plaintext copy to encrypt locally, remove the plaintext key from memory as soon as possible, and store the encrypted data key alongside the encrypted data.

3.2 The write-only variant

GenerateDataKeyWithoutPlaintext is identical to GenerateDataKey except that it does not return the plaintext copy — only CiphertextBlob. This exists for two design shapes:
  • Encrypt later. A system that must provision a key now but encrypt at some later point. When encryption time arrives, it calls Decrypt on the stored ciphertext to recover the plaintext key.
  • Distributed systems with different levels of trust. AWS's own example: one component creates containers and stores an encrypted data key with each one; a different component later decrypts that data key, encrypts the payload, writes it into the container, and destroys the plaintext key. The provisioning component never sees a plaintext data key at all.

That second shape is worth internalizing. Splitting GenerateDataKeyWithoutPlaintext (provisioning role) from Decrypt (worker role) lets you grant kms:GenerateDataKeyWithoutPlaintext to a control-plane identity that has no ability to read any data, and kms:Decrypt to a data-plane identity that cannot mint new envelopes. That is a real privilege separation, expressed entirely in key policy.

3.3 Using and destroying the plaintext key

The plaintext data key is the most dangerous object in the whole design, and its handling rules are short:
  1. Use it immediately. Encrypt, then discard. Do not carry it across a request boundary unless you have deliberately adopted caching (Section 7).
  2. Never write it to durable storage. AWS's guidance on data key caching states the rule bluntly: you should never store plaintext data keys on disk. That includes logs, crash dumps, temp files, and debug traces.
  3. Never log it. Not at DEBUG. Not "temporarily." A plaintext data key in a log aggregation system is a plaintext copy of every object that key protected, sitting in a system with a completely different access model from KMS.
  4. Zeroize where the language lets you. In languages with mutable byte buffers, overwrite the buffer after use. In managed runtimes where you cannot reliably erase memory, minimize lifetime instead — and prefer a library that does this for you.

That last point is the strongest argument for using the AWS Encryption SDK rather than hand-rolling: it handles data key lifetime, message framing, and zeroization on your behalf. AWS KMS's documentation makes the same recommendation, pointing users at the AWS Encryption SDK, the AWS Database Encryption SDK, and Amazon S3 client-side encryption rather than at bespoke code.

3.4 Storing the encrypted data key

The encrypted data key must travel with the ciphertext, and the two must stay together for the life of the data. Common placements:
* You can sort the table by clicking on the column name.
Where the ciphertext livesWhere the encrypted data key usually goes
Object storeObject metadata, or a sidecar object, or prepended to the object body
Relational or NoSQL tableA sibling column or attribute in the same row or item
Message queue or streamA message attribute, or the message header
Any of the above, via the AWS Encryption SDKInside the encrypted message — a portable, self-describing format that carries the encrypted data keys, the algorithm ID, the encryption context, and optionally a signature

The AWS Encryption SDK's encrypted-message format is the reason it is worth reaching for by default: it removes an entire category of "we lost the association between key and data" incidents by making the association structural rather than conventional.

3.5 KMS key rotation does not touch your data keys

This is the single most misunderstood interaction in envelope encryption, so it is worth stating precisely.

When a KMS key is rotated, AWS KMS creates new key material (a new backing key) and marks it as the current version for all new encrypt requests. All previous versions of the backing key remain available in perpetuity to decrypt ciphertexts that were produced under them; AWS KMS does not delete rotated key material until you delete the KMS key itself. AWS KMS does not re-encrypt anything. Your existing encrypted data keys remain encrypted under the older backing key and continue to decrypt without any change to your code, because KMS selects the correct backing material automatically — you supply no extra parameter.

The practical consequences:
  • Rotating a KMS key does not reduce the exposure of already-written data. AWS's own prescriptive guidance is direct about this: because previous key material is retained and can decrypt existing data, key rotation does not by itself provide an additional security benefit; the mechanism exists to satisfy requirements that demand rotation. If you need old ciphertext protected under new material, you must re-encrypt it — ReEncrypt re-wraps a data key under new material or a different KMS key without exposing the plaintext.
  • Automatic rotation is configurable. You can set a rotation period from 90 days to 2,560 days. Automatic rotation is supported only for symmetric encryption KMS keys with key material that AWS KMS created. AWS managed keys are rotated by AWS every year.
  • On-demand rotation exists and is limited. RotateKeyOnDemand immediately rotates the key material of a symmetric encryption KMS key. Per the AWS KMS Developer Guide you can perform on-demand rotation a maximum of 25 times per KMS key, it does not change the automatic rotation schedule, and it is not supported for asymmetric keys, HMAC keys, or keys in a custom key store. For a set of related multi-Region keys, you invoke it on the primary key.
  • ListKeyRotations is how you prove rotation happened. Use it when an auditor asks.

Warning: ScheduleKeyDeletion is not rotation. Deleting a KMS key deletes all of its backing key material, and every ciphertext ever produced under it becomes permanently unrecoverable. AWS KMS enforces a waiting period of 7 to 30 days (default 30) during which the key sits in PendingDeletion state and all cryptographic operations against it fail. You can cancel during the window with CancelKeyDeletion; you cannot recover afterward. Never treat key deletion as a cleanup task. See Section 11 for the guardrails that make accidental deletion harder.

4. Encryption Context

4.1 What it actually is

All AWS KMS cryptographic operations with symmetric encryption KMS keys accept an encryption context: an optional set of non-secret key–value pairs carrying contextual information about the data. AWS KMS uses it as additional authenticated data (AAD) to support authenticated encryption. It is cryptographically bound to the ciphertext, which means the same encryption context — a case-sensitive exact match — is required to decrypt. Supply a different one and the request fails with InvalidCiphertextException.

Three constraints define how you may use it:
  • It is not secret and it is not encrypted. It appears in plaintext in AWS CloudTrail logs. Never put sensitive data in it.
  • Keys and values must be simple literal strings. If you pass an integer or a float, AWS KMS interprets it as a string.
  • It is not available on every key type. You cannot specify an encryption context in a cryptographic operation with an asymmetric KMS key or an HMAC KMS key, because those algorithms do not support one.

AWS services that encrypt on your behalf populate it themselves. Amazon EBS, for instance, uses the volume ID: "aws:ebs:id": "vol-abcde12345abc1234".

4.2 Encryption context as an authorization control

Because the encryption context is part of the request, it can be a policy condition. Two condition keys expose it:
  • kms:EncryptionContext:<context-key> — matches on a specific key–value pair.
  • kms:EncryptionContextKeys — matches on the presence of particular keys, regardless of value.

This turns a single KMS key into a set of logically separated compartments. The following key policy statement allows a role to decrypt only when the request carries the matching application name:
{
  "Sid": "AllowDecryptForExampleAppOnly",
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::111122223333:role/RoleForExampleApp"
  },
  "Action": "kms:Decrypt",
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "kms:EncryptionContext:AppName": "ExampleApp"
    }
  }
}
The multi-tenant version of this is the pattern worth remembering: put the tenant identifier in the encryption context, then scope each tenant's role with kms:EncryptionContext:tenant bound to its own value. One KMS key, per-tenant authorization, and — because the context is bound to the ciphertext — a tenant's role physically cannot decrypt another tenant's records even if it somehow obtains the ciphertext.

A caution on Unicode. Encryption context keys and values can include Unicode characters, but if a context includes characters that are not permitted in key policies or IAM policies, you will not be able to reference it in kms:EncryptionContext:<context-key> or kms:EncryptionContextKeys at all. Keep context keys to plain ASCII identifiers.

4.3 Encryption context in grants

Grants support two encryption context constraints, and they behave differently:
  • EncryptionContextEquals — the encryption context in the request must be a case-sensitive exact match for the constraint.
  • EncryptionContextSubset — the constraint's pairs must be present in the request's context, which may contain additional pairs.
aws kms create-grant \
  --key-id 1234abcd-12ab-34cd-56ef-1234567890ab \
  --grantee-principal arn:aws:iam::111122223333:role/BatchWorker \
  --retiring-principal arn:aws:iam::111122223333:role/adminRole \
  --operations GenerateDataKey \
  --constraints EncryptionContextSubset={Purpose=BatchIngest}
EncryptionContextSubset is usually the right choice for real systems, because production encryption contexts tend to carry per-record fields (a record ID, a timestamp) that a grant author cannot enumerate in advance. Reach for EncryptionContextEquals only when the context is genuinely fixed.

4.4 Designing a context, and storing it

AWS recommends that your encryption context describe the data being encrypted or decrypted. A workable convention:
  • Tenant or owner — the isolation boundary you will want to enforce in IAM later.
  • Data classification or purpose — what the record is, so audit queries can group by it.
  • A stable identifier — something that changes if the object is moved or renamed, so tampering is detected. AWS's example is using part of a file path: if someone renames or moves the file, the encryption context value changes and decryption fails.

For storage, AWS's guidance is to keep only enough of the context alongside the ciphertext to reconstruct the full context when you need it, rather than storing the whole thing. If the context is a fully qualified path, store the fragment that is not derivable from where the object now lives.

4.5 What the AWS Encryption SDK adds

If you use the AWS Encryption SDK, three additional behaviors apply.
  • The SDK stores the encryption context in the encrypted message header, in plaintext. You do not have to supply it on decrypt; the SDK extracts it. That is convenient and it is also why the context must never contain secrets.
  • The SDK adds a reserved pair when signing. Whenever you use an algorithm suite with signing, the cryptographic materials manager adds aws-crypto-public-key to the encryption context, whose value represents the public verification key. That name is reserved and you cannot use it yourself. So a request with "Purpose"="Test", "Department"="IT" produces an actual context of "Purpose"="Test", "Department"="IT", aws-crypto-public-key=<public key>.
  • Verification is your job. Because the SDK reads the context from the message rather than from you, the decrypt function in your application should always verify that the encryption context in the decrypt response includes the encryption context you expected (or a superset of it) before returning plaintext. The AWS Encryption CLI does this for you when you pass a context to a decrypt command; the language libraries return the context and leave the check to your code. Skipping that check throws away most of the value of having a context at all.

Newer releases add a required encryption context CMM, which enforces that an encryption context is supplied on every encrypt request. It is available in version 3.x of the AWS Encryption SDK for Java, version 4.x and later for .NET, version 4.x for Python when used with the Cryptographic Material Providers Library (MPL), version 1.x for Rust, and version 0.1.x or later for Go. If you are on one of those, turning it on is the cheapest way to make "we forgot the encryption context" structurally impossible.

5. Client-Side versus Server-Side Encryption

5.1 The question is "who holds plaintext"

Both models use envelope encryption. Both use KMS keys. The difference is where the plaintext exists and which component performs the encryption.

With server-side encryption with KMS keys (SSE-KMS), you send plaintext to the AWS service over TLS. The service calls GenerateDataKey on your behalf, encrypts your data with the resulting data key, discards the plaintext data key, and stores the wrapped data key with your object. On read, it calls Decrypt and hands you plaintext. Your application never handles a data key.

With client-side encryption, your application calls GenerateDataKey itself, encrypts the payload in your own process, and stores ciphertext in the service. The storage service receives an opaque blob and has no ability to interpret it.
Client-side versus server-side encryption: who can see plaintext
Client-side versus server-side encryption: who can see plaintext

5.2 Choosing between them

* You can sort the table by clicking on the column name.
DimensionSSE-KMS (server-side)Client-side (AWS Encryption SDK / DB-ESDK)
Who encryptsThe AWS serviceYour application
Where plaintext existsIn your process and in the service's request pathOnly in your process
Code changes requiredUsually none — a bucket or table settingReal integration work in every reader and writer
Protects against a misconfigured resource policyPartially — the KMS key policy is a second gateYes — ciphertext is unreadable without the wrapping key regardless of storage permissions
Service-side features on the dataFull (server-side search, indexing, range queries, service-side processing)Restricted — the service sees only ciphertext
Portability of ciphertext across servicesTied to the service's own formatPortable — the encrypted message travels intact
Who owns decryptability foreverThe service, plus your key policyYou, plus your key policy, plus your library version and algorithm suite
Operational failure mode403 / AccessDenied on the KMS keySilent inability to decrypt if wrapping key, Region, or SDK configuration drifts

The honest summary: SSE-KMS is the default and it is a good default. It gives you at-rest encryption, a separate authorization gate in the KMS key policy, and a CloudTrail record of key use, at essentially zero integration cost. Reach for client-side encryption when one of these is true:
  • The storage service must not be able to read the data. A regulatory or contractual requirement that the platform never holds plaintext.
  • The data crosses trust boundaries in transit-at-rest form. A payload that is written by one system, sits in intermediate storage, and is read by another — where you want end-to-end protection rather than per-hop protection.
  • You need field-level rather than object-level granularity. Encrypting three columns of a table while leaving the rest queryable is a client-side problem; that is exactly what the AWS Database Encryption SDK for DynamoDB does with its per-attribute cryptographic actions.
  • You need one ciphertext format across heterogeneous stores. The AWS Encryption SDK's encrypted message is the same whether it lands in an object store, a queue, or a database column.

And the cost you accept when you do: you now own decryptability forever. Every reader must have a compatible library version, a compatible algorithm suite, access to a wrapping key, and — if you are using the Hierarchical keyring — access to a key store table. Section 12 lists the ways that promise gets broken.

5.3 The layers compose

These are not mutually exclusive, and in mature designs they stack: client-side encryption for the sensitive fields, SSE-KMS on the bucket or table underneath, TLS in transit, and a data perimeter of IAM, key policy, and organization policies around all of it. That layering is the reason Section 10 treats key policy as its own design surface rather than a checkbox.

6. AWS Encryption SDK Design Choices

The AWS Encryption SDK is a client-side encryption library that implements envelope encryption with secure defaults. It does not require AWS — you can use raw AES or raw RSA wrapping keys — but the AWS KMS integration is the reason most readers will use it. Five configuration decisions matter.

6.1 Keyrings or master key providers

There are two abstractions for "which wrapping keys do I use," and which one you get depends on your language and version.

A keyring generates, encrypts, and decrypts data keys, and you configure it with the wrapping keys it may use. A master key provider is the older alternative that returns the wrapping keys you specify. They are compatible with each other subject to language constraints, so you can encrypt in one language and decrypt in another.

Keyrings are supported in:
  • AWS Encryption SDK for C
  • AWS Encryption SDK for JavaScript
  • AWS Encryption SDK for .NET
  • Version 3.x of the AWS Encryption SDK for Java
  • Version 4.x of the AWS Encryption SDK for Python, when used with the optional Cryptographic Material Providers Library (MPL) dependency
  • Version 1.x of the AWS Encryption SDK for Rust
  • Version 0.1.x or later of the AWS Encryption SDK for Go

Master key providers are supported in Java, Python, and the AWS Encryption CLI.

The keyring types available to you (subject to the same version matrix) include the AWS KMS keyring, the AWS KMS Hierarchical keyring, the AWS KMS ECDH keyring, Raw AES, Raw RSA, Raw ECDH, and multi-keyrings that combine them.

Two behavioral rules that catch people:
  • On encrypt, every wrapping key must succeed. In all language implementations other than the AWS Encryption SDK for C, all wrapping keys in an encryption keyring or master key provider must be able to encrypt the data key. If any one fails, the encrypt call fails — so the caller needs permissions on all of them. On decrypt, only one needs to work.
  • A discovery keyring cannot encrypt. If you use a discovery keyring to encrypt, alone or in a multi-keyring, the operation fails. (The AWS Encryption SDK for C ignores a standard discovery keyring on encrypt, but fails on a multi-Region discovery keyring.)

6.2 Strict mode versus discovery mode

This is a small setting with a large security consequence.
  • Strict mode requires you to list the wrapping keys, and the SDK encrypts and decrypts with only those keys. This is the AWS Encryption SDK best practice, because it guarantees you are using the wrapping keys you intended.
  • Discovery mode lets the SDK use whatever wrapping key encrypted a given data key, as long as the caller has permission to use it.

Discovery mode is convenient and it is the wrong default for a decrypt path you care about. In strict mode, an attacker who can plant a ciphertext wrapped under a key they control cannot get your reader to decrypt it — the reader will refuse a key it was not configured for. In discovery mode, your reader will happily decrypt anything it has permission to decrypt. Use strict mode for decryption unless you have a specific reason not to, and if you must use discovery, constrain it to accounts and partitions you control.

6.3 Algorithm suites and key commitment

The AWS Encryption SDK establishes a recommended algorithm suite as the default and lets you override it. The current default is AES-GCM with an HKDF key derivation function, key commitment, and an ECDSA signature. Concretely, the two committing suites are:
* You can sort the table by clicking on the column name.
Algorithm IDMessage
format
EncryptionData key
length
Key derivationSignatureKey commitment
05 780x02AES-GCM256 bitsHKDF with SHA-512ECDSA with P-384 and SHA-384HKDF with SHA-512
04 780x02AES-GCM256 bitsHKDF with SHA-512NoneHKDF with SHA-512
03 780x01AES-GCM256 bitsHKDF with SHA-384ECDSA with P-384 and SHA-384None
01 780x01AES-GCM256 bitsHKDF with SHA-256NoneNone

All AES-GCM suites use a 12-byte initialization vector and a 16-byte authentication tag. Suites with key commitment use message format version 2; suites without use version 1. The commitment string occupies 32 bytes in the header's algorithm suite data field.

Key commitment assures that a given ciphertext always decrypts to the same plaintext. Without it, a ciphertext can, under a maliciously constructed key, decrypt validly to a different message — an attack class that matters whenever a ciphertext is shown to more than one party. This is the property that CVE-2026-6550 (Section 7.6) is about.

The important distinction the documentation draws, and that you should carry into your own vocabulary: the data key is not what encrypts your data. Suites with a key derivation function feed the data key into HKDF and use the output — the data encryption key — as the actual AES key. That is why the docs say data is encrypted "under" a data key rather than "by" it, and it is also why data key caching is only possible with KDF-based suites (Section 7.2).

The signing variant deserves a deliberate decision. ECDSA signing gives you non-repudiation: a decryptor can verify that the message came from a holder of the signing key, which matters when the encryptor and decryptor are different, mutually distrusting parties. It also costs an elliptic-curve operation per message and enlarges the message. If encryptor and decryptor are the same trusted service, the non-signing committing suite (04 78) is a defensible choice — but make it explicitly, not by accident.

6.4 Commitment policy

The commitment policy is a configuration setting that determines whether your application encrypts and decrypts with key commitment. It was introduced in version 1.7.x and has three values:
* You can sort the table by clicking on the column name.
ValueEncrypts with
commitment
Decrypts without
commitment
IntroducedWhen to use it
ForbidEncryptAllowDecryptNoYes1.7.xMigration step 1: get every host able to decrypt with commitment before any host writes with it
RequireEncryptAllowDecryptYesYes2.0.xMigration step 2: start writing with commitment while legacy ciphertext still exists
RequireEncryptRequireDecryptYesNo2.0.xSteady state. Default in 2.0.x and later

In the latest 1.x versions, ForbidEncryptAllowDecrypt is the only valid value.

The migration ordering is not arbitrary and getting it wrong causes an outage rather than a security problem. You must be able to read the new format everywhere before you start writing it anywhere. Setting the value explicitly during a 1.x → 2.x upgrade also prevents your policy from jumping straight to RequireEncryptRequireDecrypt at the moment you bump the dependency, which would make every legacy ciphertext undecryptable by the upgraded hosts.

The commitment policy also constrains which algorithm suites you may select: if you specify a suite that conflicts with your policy, the SDK returns an error.

6.5 Version and platform differences that change your design

The AWS Encryption SDK's language implementations do not have identical feature sets, and two of the differences directly affect the architecture you can build.
* You can sort the table by clicking on the column name.
ImplementationCurrent GA
major version(s)
Notes that affect design
Java3.x3.0 integrates the Material Providers Library (MPL); adds Hierarchical, ECDH, Raw, multi-keyrings, and the required encryption context CMM. Deprecates the data key caching CMM
Python4.x4.0 integrates the MPL. Keyrings require the optional MPL dependency
.NET4.x, 5.x4.0 adds the Hierarchical keyring, required encryption context CMM, and asymmetric RSA AWS KMS keyrings. 5.0 moves to MPL 2.x and requires AWS SDK for .NET v4. Does not support data key caching at all
C2.x2.3 adds multi-Region key support
JavaScript4.x4.0 requires version 3 of the JavaScript kms-client for the AWS KMS keyring
Rust1.xKeyrings and Hierarchical keyring supported
Go0.1.x or laterKeyrings and Hierarchical keyring supported
Encryption CLI4.x4.2 requires Python 3.7 or later

All 1.x.x versions are in the end-of-support phase. To upgrade from anything earlier than 1.7.x, you must go through 1.7.x first.

The design consequence to internalize now, before Section 7: if you are writing new code in .NET, data key caching is not available to you. If you are writing new code in Java on 3.x, the caching CMM is deprecated and works only through the legacy master key provider interface, not through keyrings. In both cases the intended path forward is the AWS KMS Hierarchical keyring, which is a different mechanism with different properties (Section 7.7).

7. Data Key Caching

This is the section the rest of the guide has been building toward, because it is the place where a performance decision and a security decision are the same decision.

7.1 What data key caching solves — and how to frame it

By default, the AWS Encryption SDK generates a new data key for every encryption operation. That is the most secure practice and it is the behavior you get if you do nothing. Data key caching stores the plaintext and ciphertext of data keys in a configurable cache so that a subsequent operation can reuse one instead of generating a new one.

AWS's documentation says your application may benefit if it can reuse data keys, generates numerous data keys, and finds its cryptographic operations "unacceptably slow, expensive, limited, or resource-intensive." Stripping out the commercial dimension, the technical case for caching is:
  • Latency. Every GenerateDataKey is a network round trip to a regional KMS endpoint on your write path. For a service that encrypts many small records per request, that round trip can dominate the operation.
  • Throughput and request-rate quota. Cryptographic operations draw on a shared, per-Region quota (Section 2.1). A high-volume producer — a stream ingest tier writing thousands of small records per second — can consume a meaningful share of the account's quota, and that quota is shared with everything else in the account.
  • Throttling resilience. AWS's own troubleshooting guidance for ThrottlingException on KMS lists data key caching as one of the remedies, alongside S3 Bucket Keys, backoff and retry, and a quota increase. If you are already being throttled, caching converts a dependency on KMS availability into a dependency on local memory for the duration of a cache entry's life.

The framing that keeps you honest: caching is not free performance. It is a deliberate widening of the blast radius of a single data key, purchased with round trips. AWS states the rule as: use data key caching only when it is required to meet your goals, then use the security thresholds to ensure you use the minimum amount of caching required.

7.2 The three security thresholds

When you implement caching, you configure security thresholds enforced by the caching cryptographic materials manager (caching CMM). The caching CMM returns a cached data key only when the entry conforms to all thresholds. If an entry exceeds any one of them, it is not used for the current operation and is evicted as soon as possible.
* You can sort the table by clicking on the column name.
ThresholdRequired?Valid rangeDefaultWhat it bounds
Maximum ageRequiredGreater than 0; the SDK imposes no upper limitNone — you must set itHow long an entry may be used, measured from when it was added
Maximum messages encryptedOptional1 to 2^322^32How many messages one cached data key may encrypt
Maximum bytes encryptedOptional0 to 2^63 - 12^63 - 1How many bytes one cached data key may encrypt

Four behaviors around these are easy to miss:
  • The first use of each data key, before caching, is exempt from the thresholds.
  • Maximum age is a rotation policy. AWS's guidance is to use it to limit reuse, minimize exposure of cryptographic materials, and evict data keys whose policies might have changed while they were cached. That last clause is important: a cached data key does not re-check IAM. Revoking a principal's kms:Decrypt does not invalidate the plaintext data key already sitting in that process's cache. Maximum age is the upper bound on how long a revocation takes to bite.
  • All implementations define maximum age in seconds, except the AWS Encryption SDK for JavaScript, which uses milliseconds. This is exactly the kind of unit mismatch that turns a 60-second policy into a 60-millisecond one, or a 16-hour one.
  • The current request counts toward maximum bytes. If the bytes already processed plus the current request's bytes exceed the threshold, the entry is evicted — even though it might have been usable for a smaller request.

A structural limitation: the AWS Encryption SDK only caches data keys that are encrypted using a key derivation function, and it establishes upper limits on some threshold values, specifically to ensure data keys are not reused beyond their cryptographic limits. You cannot opt out of that by configuration.

Do not disable caching by setting thresholds or capacity to zero. The Java and Python implementations provide a null cryptographic materials cache that returns a miss for every GET and ignores PUT. AWS recommends using the null cache rather than zeroing your configuration — it expresses the intent unambiguously and avoids edge cases in threshold arithmetic.
Data key cache entry matching, security thresholds, and reuse scope
Data key cache entry matching, security thresholds, and reuse scope

7.3 What makes two requests share a cache entry

To prevent the wrong data key being selected, all compatible caching CMMs require that these properties of the cached materials match the request:
  1. Algorithm suite
  2. Encryption context — even when empty
  3. Partition name — a string identifying the caching CMM
  4. (Decryption only) Encrypted data keys

Encryption and decryption use separate caches: encrypting a message does not warm a cache entry for decrypting it. And the contents differ — an encryption cache entry holds the plaintext data key, the encrypted data keys, the encryption context, the message signing key (if used), the algorithm suite, and usage counters; a decryption entry holds the plaintext data key, the signature verification key (if used), and usage counters.

The design lever hiding in that list is encryption context. Because the context is part of the cache entry identity, it lets you create subgroups of data keys within one cache even though they all come from the same caching CMM. Cached data keys are reused only when their encryption contexts match. So:
  • Want a set of operations to share a data key? Give them the same encryption context.
  • Want to guarantee two tenants never share a data key? Put the tenant identifier in the encryption context. The cache will not match across them, and the same field simultaneously enforces the boundary in IAM (Section 4.2).

If you omit the encryption context entirely, an empty context becomes part of the cache entry identifier and matches every request — which is to say, one data key is shared across everything that CMM handles. That is almost never what you want.

7.4 What you are actually trading away

Be specific about the risk, because "caching is less secure" is too vague to design against.
  • Plaintext data keys live longer in memory. By default the cache is in memory, and AWS's guidance is to minimize how long keys are saved and to limit the data that might be exposed if a key is compromised. A process memory dump, a core file, or a heap-inspection capability now yields keys that protect many messages instead of one.
  • One key protects more data. That is the entire point of the feature, and it is also the entire risk. The maximum messages and maximum bytes thresholds are your explicit statement of how much data you are willing to lose to one compromised key.
  • Authorization decisions go stale. The cached entry was authorized once. Nothing re-validates it until it ages out.
  • Cross-tenant leakage is a configuration mistake away. A shared cache with an empty or coarse encryption context is a shared key across every tenant that CMM serves.
  • Never on disk. AWS's guidance on the local cache is explicit: you should never store plaintext data keys on disk. If you substitute your own cache implementation, that constraint is yours to preserve.

7.5 When not to cache

  • Streaming or unknown-size data. The AWS Encryption SDK does not use data key caching for data of unknown size, so that the caching CMM can properly enforce the maximum bytes threshold. If you are streaming, either accept no caching or declare the size: the C implementation offers aws_cryptosdk_session_set_message_bound (set the bound larger than the estimate; if the actual message exceeds it, encryption fails), and the JavaScript for Node.js encrypt method requires plaintextLength — if you do not provide it, the data key is not cached, and if you provide a length the plaintext exceeds, the operation fails.
  • Low-volume workloads. Caching "improves performance perceptibly only in applications that generate numerous data keys with the same characteristics, including the same encryption context and algorithm suite." A workload that encrypts a handful of large objects gets nothing from a cache and takes on the risk for free.
  • Highly heterogeneous encryption contexts. If every message has a unique context, every request misses. You get the memory cost and the code complexity with a near-zero hit rate.
  • Multi-tenant processes without context separation. Covered above; do not do this.
  • Anywhere you cannot articulate the threshold values. If you cannot say why maximum age is 60 seconds rather than 6 or 6,000, you have not made a security decision — you have copied a sample.

To find out whether caching is actually helping, AWS suggests checking the frequency of calls to create new data keys in your key infrastructure's logs; with caching working, GenerateDataKey should be called only for the initial key and on cache misses.

7.6 A 2026 lesson in over-sharing a cache

On 2026-04-20, AWS published security bulletin 2026-017-AWS describing CVE-2026-6550, a key commitment policy bypass via a shared key cache in the AWS Encryption SDK for Python. AWS's description: a cryptographic algorithm downgrade in the caching layer "might allow an authenticated local threat actor to bypass key commitment policy enforcement via a shared key cache, resulting in ciphertext that can be decrypted to multiple different plaintexts."
* You can sort the table by clicking on the column name.
ItemValue
Bulletin2026-017-AWS, published 2026-04-20
IdentifierCVE-2026-6550 / GHSA-v638-38fc-rhfv
AffectedAWS Encryption SDK for Python 2.0–2.5.1, 3.0–3.3.0, 4.0–4.0.4
Fixed in3.3.1 and 4.0.5
AWS workaroundIf you must run multiple Python ESDK instances with differently configured key commitment policies, do not share a key cache between them

The mechanism is worth understanding as a design lesson rather than as an exploit: when two clients with different commitment policies share one CachingCryptoMaterialsManager, materials placed in the cache by the weaker-policy client can be served to the stricter-policy client, which then encrypts without the commitment it was configured to require. Cache entry identity, as Section 7.3 described, is matched on algorithm suite, encryption context, and partition name — the client's commitment policy was not part of that identity.

Two durable takeaways:
  1. A cache is a shared-state channel between everything that touches it. If two components have different security postures, they must not share a cryptographic materials cache. Use separate caching CMM instances with distinct partitions, or separate caches entirely.
  2. Pin and patch your encryption library like a dependency that matters. Upgrade to a fixed version — the latest AWS Encryption SDK for Python release at the time of writing is 4.0.6, published 2026-05-07 — and, as the bulletin notes, patch forked or derivative code too.

7.7 The alternative: the AWS KMS Hierarchical keyring

If your application uses the Cryptographic Material Providers Library, AWS's documentation points at the AWS KMS Hierarchical keyring as an alternative for many use cases that today rely on data key caching. It is not a caching CMM, but it caches cryptographic materials locally and reduces calls to AWS KMS in a comparable way.

The construction is different in a way that matters. Instead of caching data keys derived directly from a KMS key, the Hierarchical keyring introduces an intermediate layer of branch keys, persisted in a DynamoDB table that acts as your key store. To create one you provide:
  • A key store name — the DynamoDB table holding your branch keys. Its partition key is branch-key-id and its sort key is type.
  • A cache limit TTL — how long a branch key materials entry may be used before it expires. This "dictates how often the client calls AWS KMS to authorize use of the branch keys." After the TTL expires the entry is never served and is evicted.
  • A branch key identifier — either a statically configured branch-key-id, or a branch key ID supplier that determines which branch key is required from fields stored in the encryption context.
  • Optionally, a cache — Default, MultiThreaded, StormTracking, or Shared.

AWS strongly recommends the branch key ID supplier for multitenant databases where each tenant has their own branch key, and it lets you refer to a branch key by a friendly name such as tenant1 instead of a UUID. That is the cleanest expression of per-tenant key separation available in the client-side toolkit.

Three constraints before you adopt it:
  • Content encrypted with the Hierarchical keyring can only be decrypted with the Hierarchical keyring. This is a one-way architectural commitment.
  • Do not delete the DynamoDB table that persists your branch keys. If you do, you will be unable to decrypt any data encrypted using the Hierarchical keyring. Treat that table with the same care as the KMS key itself: backups, deletion protection, and a resource policy that denies DeleteTable.
  • The logical key store name is cryptographically bound to the data and cannot be changed after the first user defines it. AWS strongly recommends making it identical to the DynamoDB table name, and warns that it is displayed in plaintext in AWS KMS CloudTrail events as tablename — so do not put anything confidential in it.

Choosing between them:
* You can sort the table by clicking on the column name.
Caching CMM (data key caching)AWS KMS Hierarchical keyring
AvailabilityC, Java 2.x (deprecated in 3.x), JavaScript, Python. Not in .NETJava 3.x, .NET 4.x+, Python 4.x with MPL, Rust 1.x, Go 0.1.x+
Extra infrastructureNone (in-memory cache)A DynamoDB key store table you must operate and protect
Multi-tenant separationVia encryption context in the cache entry identityVia branch keys, one per tenant, selected by a branch key ID supplier
Reuse boundMaximum age, messages, bytesCache limit TTL on branch key materials
Ciphertext compatibilityStandard AWS Encryption SDK messageDecryptable only with the Hierarchical keyring
Best fitExisting caching deployments; languages without MPLNew builds on MPL-era versions, especially multi-tenant

For a new application on a current SDK version, the Hierarchical keyring is generally the better-supported path. For an existing caching deployment that works, the migration is a ciphertext-format commitment and should be planned as one.

8. Service-Level Optimizations

You do not always have to implement envelope encryption yourself. Several AWS services do it internally, and understanding what they do — and what they change — prevents surprises.

8.1 Amazon S3 Bucket Keys

Without an S3 Bucket Key, S3 uses an individual AWS KMS data key for every object, which means a call to AWS KMS every time a request is made against a KMS-encrypted object. Workloads accessing millions or billions of SSE-KMS objects generate correspondingly large volumes of KMS requests.

When you configure a bucket to use an S3 Bucket Key, AWS generates a short-lived bucket-level key from AWS KMS and temporarily keeps it in S3. That bucket-level key creates data keys for new objects during its lifecycle, which reduces the traffic from S3 to AWS KMS. In quota terms: the same objects, far fewer requests against your account's shared cryptographic operations quota.

This is the same trade as Section 7 — fewer round trips in exchange for one key covering more objects — and it carries three consequences you must design around.

1. The encryption context changes from the object ARN to the bucket ARN. AWS's own warning is explicit: if your existing IAM policies or AWS KMS key policies use your object ARN as the encryption context to refine or limit access to your KMS key, these policies will not work with an S3 Bucket Key. Update them to use the bucket ARN before enabling the feature. Enabling a bucket key on a bucket whose key policy is scoped to object ARNs is a self-inflicted outage.

2. Subsequent requests do not validate against the KMS key policy. AWS states it plainly: "By design, subsequent requests that take advantage of this bucket-level key do not result in AWS KMS API requests or validate access against the AWS KMS key policy." Unique bucket-level keys are fetched at least once per requester so that the requester's access is captured in a KMS CloudTrail event — and S3 treats callers as different requesters when they use different roles or accounts, or the same role with different scoping policies. But within a bucket key's lifetime, that principal's continued access is not being re-checked at KMS. If your security model depends on the KMS key policy being evaluated on every single object read, a bucket key weakens it.

3. Your CloudTrail signal thins out. After enabling, your AWS KMS CloudTrail events log the bucket ARN instead of the object ARN, and you see fewer KMS CloudTrail events for SSE-KMS objects. Any detection rule that counted Decrypt events per object, or that pivoted on object ARNs in the encryption context, needs rewriting.

Other operational facts:
  • S3 Bucket Keys are not supported for DSSE-KMS.
  • Existing objects do not use the bucket key. Only new objects do; to apply it to existing objects, use CopyObject.
  • S3 shares a bucket key only among objects encrypted by the same AWS KMS key. Bucket keys are compatible with KMS-created keys, imported key material, and key material backed by custom key stores.
  • Configuration surface: PutBucketEncryption accepts BucketKeyEnabled in ServerSideEncryptionRule; PutObject, CopyObject, CreateMultipartUpload, and POST Object accept the x-amz-server-side-encryption-bucket-key-enabled request header to override the bucket setting per object; HeadObject, GetObject, and the multipart operations return it as a response header. In CloudFormation, AWS::S3::Bucket exposes BucketKeyEnabled under its encryption property.
  • Replication caveat: if the source object is unencrypted and the destination bucket uses a bucket key with SSE-KMS, the replica is encrypted in the destination — which makes the source and replica ETag values differ. Applications that compare ETags must accommodate that.
Resources:
  RecordsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: aws:kms
              KMSMasterKeyID: !Ref RecordsKey
            BucketKeyEnabled: true

8.2 Secrets and configuration stores

The two managed stores encrypt differently, and the difference is instructive:
  • AWS Secrets Manager always uses envelope encryption. Whenever a secret value changes it calls GenerateDataKey, encrypts the value outside KMS with the resulting data key, discards the plaintext key, and stores the encrypted data key in the secret's metadata.
  • Systems Manager Parameter Store depends on the tier. A standard-tier SecureString (up to 4,096 bytes — the same 4 KB ceiling as Encrypt, and not a coincidence) is encrypted directly under the KMS key. An advanced-tier SecureString uses envelope encryption via the AWS Encryption SDK, because the larger value is encrypted with a local data key rather than a direct KMS call over the whole payload.

That is the 4 KB boundary from Section 2.1, visible as a product decision. For where to put which value, see the AWS Secrets Manager and Parameter Store Decision Guide.

8.3 Field-level encryption in DynamoDB

The AWS Database Encryption SDK for DynamoDB (formerly the DynamoDB Encryption Client) provides attribute-level client-side encryption. It encrypts attribute values and then calculates a signature over the attributes, and you declare per-attribute cryptographic actions:
* You can sort the table by clicking on the column name.
Cryptographic actionEffect
ENCRYPT_AND_SIGNValue is encrypted and included in the signature
SIGN_ONLYValue stays plaintext but is authenticated. Partition and sort attributes must be SIGN_ONLY
SIGN_AND_INCLUDE_IN_ENCRYPTION_CONTEXTSigned and surfaced into the encryption context (usable by a branch key ID supplier). If you use it for any attribute, the partition and sort attributes must also use it
DO_NOTHINGExcluded from encryption and from the signature; must be declared as an allowed unsigned attribute

Two operational notes: the logical table name is cryptographically bound to all data in the table (so a restore under a different table name still decrypts, provided you keep the logical name stable), and the AWS Database Encryption SDK does not support PartiQL. Design your access patterns accordingly, and see the Amazon DynamoDB Master Index for the data modeling side.

8.4 Choosing the layer

* You can sort the table by clicking on the column name.
RequirementReach for
At-rest encryption with an auditable key, minimum effortSSE-KMS with a customer managed key
High-volume object access against SSE-KMS, quota pressureSSE-KMS plus S3 Bucket Keys — after auditing encryption-context conditions
The storage service must never see plaintextAWS Encryption SDK (client-side)
Only some fields are sensitive, the rest must stay queryableAWS Database Encryption SDK for DynamoDB
Many small writes per second, latency-sensitive, client-sideAWS Encryption SDK with caching CMM or Hierarchical keyring
Rotating credentials rather than bulk dataAWS Secrets Manager

9. Multi-Region and Cross-Account Design

9.1 When a multi-Region key is the right answer

A multi-Region key (MRK) is a set of interoperable KMS keys in different Regions of the same AWS partition that share a key ID and key material, so you can encrypt data in one Region and decrypt it in a different Region without making a cross-Region call or exposing the plaintext.

You create a primary key with CreateKey and MultiRegion set to true (or in the console); multi-Region keys have a distinctive key ID beginning with mrk-, which you can use to identify them programmatically. You then call ReplicateKey to create replica keys in other Regions.

The decisive properties:
  • You decide single-Region or multi-Region only at creation. You cannot convert an existing single-Region key into a multi-Region key. AWS notes this design ensures data protected with existing single-Region keys maintains its data residency and sovereignty properties.
  • Only a primary key can be replicated, and automatic key rotation can be enabled or disabled only on the primary.
  • You can schedule deletion of a primary key at any time, but AWS KMS will not delete it until all of its replica keys are deleted.
  • Primary and replica keys do not differ in any cryptographic property and can be used interchangeably.
  • Because multi-Region keys have different security properties from single-Region keys, AWS recommends creating one only when you plan to replicate it.

The workload shapes that justify an MRK: an active-active application whose records must be readable in whichever Region serves the user; an asynchronous pipeline that writes in one Region and processes in another; a disaster recovery design where the standby Region must be able to read the primary's data without a cross-Region KMS dependency. For the architecture around those, see the AWS Multi-Region Active-Active Architecture Guide.

9.2 What synchronizes and what does not

This is where MRK designs go wrong, so hold the two lists separately.
* You can sort the table by clicking on the column name.
PropertySynchronized across related MRKs?
Key IDYes (shared property)
Key materialYes (shared property)
Key specYes (shared property)
Key usageYes (shared property)
Key material originYes (shared property)
Automatic key rotation statusYes (shared property)
Key policyNo
GrantsNo
AliasesNo
TagsNo
Key state (enabled / disabled)No
DescriptionNo

Key ARNs differ only in the Region field.

The failure this produces is quiet and specific: you replicate a key to a second Region, your infrastructure-as-code updates the primary's key policy in a later change, and the replica's policy stays where it was. The data still decrypts in Region A and starts returning AccessDenied in Region B — or, worse, keeps allowing a principal in Region B that you believed you had revoked. Manage every replica's key policy in the same infrastructure-as-code change set as the primary's, and treat "key policy drift between replicas" as a config rule you actually check.

The same applies to key state: DisableKey on a primary does not disable its replicas. If you are disabling a key as an incident response step, you must disable each related key explicitly.

9.3 Cross-account access

Cross-account use of a KMS key is a two-sided grant and both sides are mandatory:
  1. In the account that owns the key, add a key policy statement whose Principal is the external account (or specific roles in it). Key policies do not specify a resource — the resource is the key the policy is attached to.
  2. In the external account, attach IAM policies to the specific users and roles that should be able to use the key. Naming the external account in the key policy delegates the decision to that account's IAM administrators; it does not by itself grant any principal anything.
{
  "Sid": "AllowExternalAccountToUseThisKey",
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::444455556666:root"
  },
  "Action": [
    "kms:Encrypt",
    "kms:Decrypt",
    "kms:ReEncrypt*",
    "kms:GenerateDataKey*",
    "kms:DescribeKey"
  ],
  "Resource": "*"
}
Three things to know:
  • Cross-account is limited to cryptographic and grant actions. If the principal is in a different account than the KMS key, essentially only cryptographic and grant operations are supported — administrative operations are not.
  • Opt-in Regions matter. Permissions given to an external account are effective only if that account is enabled in the Region that hosts the KMS key.
  • Key policies are regional. A key policy is in effect only in the Region that contains the key — a recurring source of confusion when the same template is deployed to several Regions.

Layer this with your organization-level controls: SCPs and RCPs can bound what any principal in the organization can do with keys, and a data perimeter can deny key use from outside expected networks or organizations. See AWS Organization Guardrails: SCP and RCP for that layer.

9.4 Grants, and their consistency window

Grants are the mechanism for delegating time-bound, narrowly scoped permissions on a key — typically to an AWS service acting on your behalf, or to a principal in another account, without editing the key policy for every case. Combine them with the encryption context constraints from Section 4.3 for tight scoping.

The property to design around: when you create, retire, or revoke a grant, there might be a brief delay — usually less than five minutes — until the grant is available throughout AWS KMS. This is eventual consistency, and it cuts both ways. A newly created grant may not be usable immediately (use the returned grant token in the interim to make the permission effective right away), and — more importantly for incident response — RevokeGrant is not instantaneous. If you are revoking access during an incident, treat the grant revocation as the start of a window, not the end of one, and pair it with a control that takes effect immediately.

10. Key Policy and IAM Design

10.1 The layers that all have to agree

Access to a KMS key is decided by the combination of the key policy (the resource-based policy that every KMS key has), IAM identity-based policies, grants, and any organization-level policies that scope the request: SCPs, RCPs, and permission boundaries. Every one of those can deny.

The mechanics of how those combine — evaluation order, explicit deny, the interaction of boundaries and session policies — are covered in depth in IAM Policy Evaluation Logic Step by Step and are not repeated here. What is specific to KMS:
  • Every KMS key has a key policy. If you do not provide one, AWS KMS creates one for you, and the default differs depending on whether you create the key in the console or through the API. AWS recommends editing the default to match your least-privilege requirements rather than accepting it.
  • The key policy is the gate for IAM to matter at all. To use an identity-based policy to control access to a KMS key, the key policy must allow the account to use IAM policies — either through a statement that enables IAM policies, or by naming principals explicitly in the key policy. This is the number-one cause of "my IAM policy says kms:Decrypt but I still get AccessDenied."

Warning: editing a key policy is one of the ways to lose access to a key permanently. Removing the statement that enables IAM policies, or the principals who can administer the key, can leave nobody able to change the policy back. Change key policies through reviewed infrastructure-as-code, never by hand in the console on a production key, and confirm with IAM Access Analyzer or a dry run before applying.

If you are authoring key policies, the AWS KMS Key Policy Builder Tool on this site generates statements for the common patterns.

10.2 Constrain by service with kms:ViaService

The kms:ViaService condition key limits use of a KMS key to requests from specified AWS services. The operation must be a KMS key resource operation, and — an important caveat — this condition key only applies for forward access sessions, meaning it works when a service calls KMS carrying your identity, not when a service calls KMS with its own service principal.
{
  "Sid": "AllowUseOnlyThroughS3AndSecretsManager",
  "Effect": "Allow",
  "Principal": {
    "AWS": "arn:aws:iam::111122223333:role/payments-app"
  },
  "Action": [
    "kms:Encrypt",
    "kms:Decrypt",
    "kms:ReEncrypt*",
    "kms:GenerateDataKey*",
    "kms:DescribeKey"
  ],
  "Resource": "*",
  "Condition": {
    "StringEquals": {
      "kms:ViaService": [
        "s3.us-east-1.amazonaws.com",
        "secretsmanager.us-east-1.amazonaws.com"
      ]
    }
  }
}
This is a genuinely strong control: it means a leaked credential for payments-app cannot call kms:Decrypt directly to unwrap data keys it obtained elsewhere — only requests arriving through S3 or Secrets Manager are permitted. Note that it does not work for client-side encryption, where your application calls KMS directly; combining direct-call and via-service access on one key requires two statements with different conditions.

10.3 Key granularity

How many KMS keys should you have? The answer follows from what a key policy can express, because the key is the smallest unit you can revoke, audit separately, or share across accounts.

A workable default: one customer managed key per sensitivity domain per Region per account, where a sensitivity domain is a set of data with the same authorization story — the same readers, the same retention, the same regulatory treatment.
* You can sort the table by clicking on the column name.
SignalSplit into a separate key
A different set of principals should be able to decryptYes
The data must be shared with a different AWS accountYes
You would ever want to revoke access to this data without affecting othersYes
Compliance requires separate key custody or separate auditYes
Different retention or deletion timelinesYes
Only the record type differs, with identical readersNo — use the encryption context (Section 4.2)
Only the tenant differs, within one applicationUsually no — use the encryption context, or branch keys (Section 7.7)

The failure mode at both extremes is real. One key for everything means you cannot revoke anything without revoking all of it. A key per tenant in a large multi-tenant system means thousands of key policies to keep consistent, per Region, and a request-rate quota shared across all of them anyway. Encryption context and branch keys exist precisely so that you can get per-tenant separation without per-tenant keys.

11. Auditing and Detection

11.1 What CloudTrail gives you

AWS KMS logs its operations to AWS CloudTrail, including the encryption context, so a log entry shows which KMS key was used to encrypt or decrypt the specific data referenced by that context. This is the payoff for the design work in Section 4: if your encryption context carries the tenant, the data classification, and a stable identifier, then your CloudTrail history is a queryable record of what was decrypted, not merely that something was.

Because the encryption context is logged, it must not contain sensitive information — the same rule, restated from the audit side.

For centralizing and querying these logs across an organization, see the AWS Centralized Logging and Audit Architecture Guide.

11.2 Key last-usage tracking, and its limits

On 2026-04-27, AWS announced that AWS KMS now tracks the last usage of all KMS keys. You can view the most recent cryptographic operation performed with a KMS key in the AWS KMS console or through the GetKeyLastUsage API, which returns the timestamp, the operation type, and the associated AWS CloudTrail event ID. It is available in all Regions where AWS KMS is offered, including the AWS GovCloud (US) Regions and the China Regions. This removes the previous need to query and analyze CloudTrail logs manually just to answer "is this key still in use?"

Read the limitation carefully, because it is exactly the limitation envelope encryption creates. AWS's warning: these strategies are effective only for AWS principals and AWS KMS operations, and cannot detect use of the key that does not involve API calls to AWS KMS. Specifically — "after a data key is generated from a symmetric KMS key, its subsequent use for local encryption or decryption outside of AWS KMS is not reflected in the last usage information."

In other words: the more successfully you optimize your KMS call volume, the less your usage telemetry tells you. A workload using an aggressive caching CMM, or a bucket with S3 Bucket Keys enabled, will show far fewer KMS events than the amount of data actually being encrypted and decrypted. "This key has not been used in 90 days" does not mean "no data was decrypted under this key in 90 days." Treat last-usage data as a signal about KMS calls, and answer "is this data still live?" from the application side.

11.3 Guardrails against accidental disable and delete

The same 2026 release added the condition key kms:TrailingDaysWithoutKeyUsage, a numeric, single-valued condition that applies to the DisableKey and ScheduleKeyDeletion operations, usable in both key policies and IAM policies.

It represents the number of trailing days without cryptographic operations on the key, calculated from the last successful cryptographic operation, or from the key's creation date or TrackingStartDate if it has never been used. The value is always rounded down — a key last used 89.9 days ago yields 89.

Used as a deny, it becomes a safety interlock: refuse to schedule deletion of a key that has been used recently.
{
  "Sid": "DenyDeleteOrDisableOfRecentlyUsedKeys",
  "Effect": "Deny",
  "Principal": "*",
  "Action": [
    "kms:ScheduleKeyDeletion",
    "kms:DisableKey"
  ],
  "Resource": "*",
  "Condition": {
    "NumericLessThan": {
      "kms:TrailingDaysWithoutKeyUsage": "90"
    }
  }
}
Warning: this is a guardrail, not a guarantee — it inherits the blind spot in Section 11.2. A key whose data keys are heavily cached may show no recent KMS usage while protecting live production data, and this condition would happily permit its deletion. Pair it with the 7-to-30-day PendingDeletion waiting period, a CloudWatch alarm on use of a key pending deletion, and a human review gate.

11.4 What to actually alert on

* You can sort the table by clicking on the column name.
SignalWhy it matters
Decrypt volume for a key rising sharply against its own baselineBulk exfiltration reads data through the same decrypt path as legitimate access
Decrypt with an encryption context that does not match the caller's expected scopeA principal reading another tenant's or another classification's data
Decrypt or GenerateDataKey from a principal that has never used the key beforeNew access path, intended or otherwise
Cryptographic operations from an unexpected account in recipientAccountIdCross-account use you did not authorize
ScheduleKeyDeletion, DisableKey, PutKeyPolicy, RevokeGrant on a production keyDestructive control-plane changes; these should be rare and always reviewed
Any use of a key in PendingDeletion stateSomething still depends on a key you were about to destroy
ThrottlingException rate on KMSQuota pressure, and possibly an unintended caching regression
CreateGrant to an unexpected grantee principalGrants bypass the readability of key policy review

Two blind spots to hold in mind while writing detections. First, per Section 8.1, enabling S3 Bucket Keys reduces the number of KMS CloudTrail events and switches the logged ARN from object to bucket — any rule tuned before that change needs retuning after it. Second, as Section 7.4 noted, a cached plaintext data key is not re-authorized; a decrypt that succeeds from cache generates no CloudTrail event at all.

12. Failure Modes and Anti-Patterns

Writing a plaintext data key somewhere durable. Logs, crash dumps, temp files, metrics labels, exception messages. AWS's rule is unqualified: never store plaintext data keys on disk. A plaintext data key in a log system is a plaintext copy of everything that key protected, in a system with a completely different access model. Scan for it in code review; it is almost always an accident.

Skipping the encryption context. With no context you have no AAD binding, no kms:EncryptionContext condition to write, no audit dimension in CloudTrail, and — if you use a caching CMM — an empty context that matches every request, meaning one data key shared across everything.

Not verifying the encryption context on decrypt. The AWS Encryption SDK returns the context with the plaintext; if your code does not check that the returned context includes what you expected, you have paid for the context and thrown away the guarantee.

Sharing a cryptographic materials cache across differing security postures. This is CVE-2026-6550's shape (Section 7.6): a shared cache is a shared-state channel, and the cache entry identity does not include the client's commitment policy. Separate CMMs, separate partitions, or separate caches.

Treating cache thresholds as tuning knobs rather than security decisions. Maximum age is a rotation policy and the upper bound on how long a revoked principal keeps working. Maximum messages and maximum bytes are your statement of how much data one compromised key may expose. Copying a sample's 60 without knowing whether it is seconds or milliseconds (JavaScript uses milliseconds; everything else uses seconds) is how a policy silently becomes a thousand times looser or tighter than intended.

Expecting caching to help a streaming workload. The AWS Encryption SDK does not cache data keys for data of unknown size. If your throughput problem is a stream, either declare the message size or accept that the cache will not engage.

Enabling S3 Bucket Keys without auditing encryption-context conditions. The context changes from the object ARN to the bucket ARN. Policies scoped to object ARNs stop working. Audit first, migrate the conditions, then enable.

Assuming per-request key policy evaluation with S3 Bucket Keys enabled. By design, subsequent requests using the bucket-level key do not make KMS API requests and do not validate access against the KMS key policy. If your model requires the key policy on every object read, do not enable bucket keys.

Letting multi-Region replica key policies drift. Key policies, grants, aliases, tags, and key state are not synchronized between related multi-Region keys. Manage every replica's policy in the same change set as the primary's, and remember that disabling a primary during an incident does not disable its replicas.

Trying to convert a single-Region key to multi-Region. You cannot. This is decided at creation. If you need multi-Region later, you create a new MRK and re-encrypt — plan for that.

Believing key rotation protects existing data. It does not. Previous backing key material is retained in perpetuity and continues to decrypt old ciphertext. If you need existing data under new material, re-encrypt it (ReEncrypt re-wraps the data key without exposing plaintext).

Treating grant revocation as immediate. There is a delay of usually less than five minutes before a revoke propagates. During an incident, that window is real.

Deleting the Hierarchical keyring's key store table. If you delete the DynamoDB table that persists your branch keys, you cannot decrypt anything encrypted with that Hierarchical keyring. Protect it like a KMS key.

Scheduling key deletion as cleanup. ScheduleKeyDeletion destroys all backing key material after a 7-to-30-day waiting period, and every ciphertext produced under that key becomes permanently unrecoverable. Before you ever consider it: check GetKeyLastUsage, remembering that it cannot see local use of already-issued data keys (Section 11.2); check CloudTrail history; deploy the kms:TrailingDaysWithoutKeyUsage guardrail; use the full 30-day window; and set a CloudWatch alarm on any use of the key while it is pending deletion. Prefer DisableKey as a reversible first step — noting that this too breaks every workload depending on the key, so it is a controlled outage, not a no-op.

Running an unpatched encryption library. Client-side encryption libraries carry cryptographic guarantees; a defect in one is a defect in your data protection. Pin versions, watch AWS security bulletins, and upgrade deliberately — including any forked or vendored copies.

13. Frequently Asked Questions

Should I use the AWS Encryption SDK, or call GenerateDataKey and Decrypt myself?

Use the library unless you have a specific reason not to. It handles data key lifetime and zeroization, produces a portable self-describing encrypted message that keeps the wrapped key and ciphertext together, implements key commitment and the current recommended algorithm suite, and gives you keyrings, commitment policy, and the required encryption context CMM as configuration rather than code. AWS's own KMS documentation recommends a client-side encryption library over bespoke code for this pattern.

Does rotating my KMS key mean my old data is re-encrypted?

No. Rotation creates new key material that becomes current for new encrypt requests; all previous versions remain available in perpetuity to decrypt existing ciphertext, and AWS KMS does not delete rotated material until you delete the key. Nothing in your code changes, because KMS picks the right backing material automatically. If you need old data protected under new material, you must re-encrypt it explicitly — ReEncrypt re-wraps a data key without exposing plaintext.

Is data key caching safe to turn on?

It is safe in the sense that AWS supports it and it enforces bounds you configure — and it is a deliberate reduction in security relative to the default. The AWS Encryption SDK generates a new data key per operation by default because that is the most secure practice, and its guidance is to use caching only when required to meet your goals, with the minimum thresholds that achieve them. If you cannot state why your maximum age, maximum messages, and maximum bytes values are what they are, you are not ready to turn it on. And note that the AWS Encryption SDK for .NET does not support it, and version 3.x for Java deprecates the caching CMM in favor of the Hierarchical keyring.

What is the difference between data key caching and the AWS KMS Hierarchical keyring?

The caching CMM caches data keys derived directly from your KMS key, bounded by age, message count, and byte count. The Hierarchical keyring introduces branch keys persisted in a DynamoDB key store and caches the branch key materials locally, bounded by a cache limit TTL that determines how often the client calls AWS KMS to authorize branch key use. The Hierarchical keyring needs infrastructure you must operate and protect, and its ciphertext can only be decrypted with the Hierarchical keyring — but it is the supported path on MPL-era SDK versions and it expresses per-tenant separation cleanly through a branch key ID supplier.

Do I need a separate KMS key per tenant in a multi-tenant application?

Usually not. Put the tenant identifier in the encryption context and scope each tenant's role with kms:EncryptionContext:tenant; because the context is bound to the ciphertext as AAD, a tenant's role cannot decrypt another tenant's records. Split into separate keys when tenants need genuinely separate custody, separate audit, cross-account sharing, or independently revocable access. If you are on an MPL-era SDK version, per-tenant branch keys under one KMS key are a middle path.

What breaks when I turn on S3 Bucket Keys?

Three things. The encryption context changes from the object ARN to the bucket ARN, so any IAM or KMS key policy condition scoped to object ARNs stops working — fix those first. Subsequent requests using the bucket-level key do not make KMS API calls and, by design, do not validate access against the KMS key policy. And your KMS CloudTrail events log the bucket ARN instead of the object ARN, and there are fewer of them, so detections tuned on the old volume and shape need retuning. Also note bucket keys are not supported for DSSE-KMS and do not apply to objects already in the bucket.

Can I turn an existing key into a multi-Region key?

No. Whether a key is single-Region or multi-Region is fixed at creation, and AWS notes this preserves the data residency and sovereignty properties of data already protected by single-Region keys. Create a new multi-Region key and re-encrypt, and remember that a primary key's scheduled deletion will not complete until all replicas are deleted.

If GetKeyLastUsage says a key has not been used in 90 days, is it safe to delete?

No — that is exactly the question the feature cannot answer. Last-usage tracking sees AWS KMS API calls only, and after a data key is generated, its subsequent local use for encryption or decryption outside AWS KMS is not reflected. A key whose data keys are cached, or a bucket with S3 Bucket Keys enabled, will show far less KMS activity than the data volume it protects. Corroborate from the application side, deploy the kms:TrailingDaysWithoutKeyUsage guardrail, use the full 30-day waiting period, alarm on any use of a key pending deletion, and remember that deletion is permanent and unrecoverable.

14. Summary

Envelope encryption is not an optimization layered on top of AWS KMS; it is the intended way to use it. KMS holds a root key that never leaves its hardware security modules in plaintext, hands out short-lived data keys on request, and stays out of the path of your bulk data. Everything else in this guide is a decision about how far to push that separation.

The decisions, condensed:
  • Direct Encrypt only for values under 4,096 bytes. Everything else uses a data key, and every cryptographic operation draws on a shared, per-Region request-rate quota.
  • Treat the plaintext data key as the most dangerous object in your process. Use it, discard it, never persist it, never log it. Prefer a library that manages its lifetime for you.
  • Always supply an encryption context, and always verify it on decrypt. It is AAD, an IAM condition, an audit dimension, and a cache partition key — four controls for one design decision.
  • Default to SSE-KMS. Move to client-side encryption when the storage service genuinely must not hold plaintext, when you need field-level granularity, or when ciphertext must stay opaque across a pipeline.
  • On the AWS Encryption SDK, be explicit about four settings: keyring versus master key provider, strict versus discovery mode, the algorithm suite, and the commitment policy — and know your language's version constraints, because .NET has no data key caching and Java 3.x deprecates the caching CMM.
  • Data key caching is a blast-radius decision. Maximum age is a rotation and revocation bound; maximum messages and maximum bytes are exposure bounds. Encryption context is what stops one cached key from spanning tenants. Never share a cache between clients with different security postures — CVE-2026-6550 is the documented cost of doing so.
  • Every request-reduction mechanism thins your telemetry. S3 Bucket Keys change the logged ARN and reduce event volume; cached data keys generate no events at all. Rewrite detections when you adopt them, and never conclude "unused" from KMS telemetry alone.
  • Multi-Region keys synchronize key material, not key policies. Policies, grants, aliases, tags, and key state drift independently. Manage replicas in the same change set as the primary.
  • Decryptability is a promise you make to your future self. Key deletion, key store deletion, policy drift, an unmigrated commitment policy, and an unpatched library are all ways to break it — and all of them are silent until the moment you need the data.

Get the encryption context right and most of the rest of this becomes tractable. Get it wrong and you will spend the life of the system wishing you had.

15. References

Related Articles


References:
Tech Blog with curated related content

Written by Hidekazu Konishi