skip to content

KMS Keys & Envelope Encryption

KMS holds keys that never leave the service; instead of encrypting my data with them, I ask for a data key and encrypt locally. That envelope pattern, plus the key-policy-versus-IAM authorization split, is the heart of AWS encryption questions.

part ofAWSoverview, primer and where to startread it →
on this pageshow

questions

6

AWS KMS caps the plaintext of a single symmetric Encrypt call at a few kilobytes. Walk through how you encrypt a 500 MB file under a KMS key instead: which API you call, what it returns, and what you store next to the ciphertext.

level: middleimportance: must knowfreq 75%

answer

  1. KMS holds keys, never your data
  2. one key returned twice
  3. Plaintext plus CiphertextBlob
  4. store the blob, wipe the plaintext

basics

~20 s

Call kms:GenerateDataKey. KMS returns one data key twice, as plaintext and encrypted under your KMS key. Encrypt the file locally with the plaintext copy, wipe it from memory, and store the encrypted copy beside the ciphertext.

solid answer

~40 s

KMS never sees the file. I call `kms:GenerateDataKey` against the key (usually by alias) with `KeySpec` `AES_256`. The response has three fields: `Plaintext` — the raw data key, `CiphertextBlob` — the same key encrypted under the KMS key, and `KeyId`. I encrypt the 500 MB locally with the plaintext data key using a normal AES library, then zero the plaintext key out of memory and persist the `CiphertextBlob` with the object — file header, S3 object metadata, or a column next to the row. To read it back I take the blob, call `kms:Decrypt`, get the data key, decrypt locally, and discard the key again. That is envelope encryption: one KMS request per object rather than per byte, and revocation happens by taking away `kms:Decrypt` rather than by touching the data.

code

bash · 11 lines
bash
# One call returns the same data key twice
aws kms generate-data-key --key-id alias/backups --key-spec AES_256
# {
#   "CiphertextBlob": "AQIDAHh...",   <- store this next to the file
#   "Plaintext": "3Vv0k...",          <- encrypt with this, then wipe it
#   "KeyId": "arn:aws:kms:eu-west-1:111122223333:key/1234abcd-12ab-34cd-56ef-1234567890ab"
# }

# Later, to read the object back
aws kms decrypt --ciphertext-blob fileb://data-key.enc --key-id alias/backups \
  --query Plaintext --output text

go deeper

for a junior

Know the shape of the flow: ask KMS for a data key, get a usable copy and an encrypted copy, encrypt locally, and keep the encrypted copy with the file. Say plainly that KMS never receives the data itself.

for a middle

Be ready to name kms:GenerateDataKey, the Plaintext/CiphertextBlob/KeyId fields, and the matching kms:Decrypt call, and to explain why the 4 KB Encrypt cap exists at all rather than treating it as an arbitrary rule.

for a senior

Show the operational side: where the blob is persisted, how the plaintext key is wiped, what CloudTrail records per unwrap, and how you keep per-object GenerateDataKey calls from hitting the Region's request-rate quota.

for a principal

Own the tradeoff behind data-key reuse. Decide how many objects one data key may protect and for how long, and be explicit that caching trades blast radius and key lifetime for request cost and quota headroom.

## Why the Encrypt API is not the answer AWS KMS is a key service, not a data service. The key material of a symmetric KMS key is generated inside KMS's HSMs and cannot be exported; every `Encrypt` or `Decrypt` call therefore ships your bytes across the network to a regional KMS endpoint and back. That design forces a hard cap: a single symmetric `Encrypt` call accepts at most 4 KB (4,096 bytes) of plaintext. A 500 MB file is three orders of magnitude past that, and even if the cap did not exist you would be pushing every byte of customer data through a shared, throttled, per-request-billed service. ## What GenerateDataKey actually returns `kms:GenerateDataKey` is the call that makes the pattern work. You pass a key identifier — a key ID, ARN, or an alias such as `alias/backups` — and either `KeySpec` (`AES_256` or `AES_128`) or `NumberOfBytes`. KMS generates a fresh random key inside the HSM and hands it back in **two representations of the same key**: - `Plaintext` — the raw bytes, base64-encoded in the JSON response. This is the key you actually encrypt with. - `CiphertextBlob` — that same key encrypted under your KMS key. KMS keeps no copy of either. - `KeyId` — the ARN of the KMS key that wrapped it. The flow is then: call `GenerateDataKey`; encrypt the file locally with the plaintext data key using an ordinary AES implementation; zero the plaintext key; write the `CiphertextBlob` somewhere durable that travels with the ciphertext. ```text GenerateDataKey -> { Plaintext, CiphertextBlob } encrypt file with Plaintext, wipe Plaintext store CiphertextBlob alongside file later: Decrypt(CiphertextBlob) -> Plaintext -> decrypt file -> wipe ``` The blob is not secret in the way the plaintext key is — it is useless to anyone without `kms:Decrypt` on the KMS key — so it is safe in an object-metadata field, a file header, or a database column. ## Decrypting To read the object you send the blob to `kms:Decrypt`. For a symmetric key you do not have to tell KMS which key was used: the key ARN is embedded in the blob. Passing `KeyId` explicitly anyway is the safer habit, because it stops a blob supplied by an attacker from steering your process at a key they control. KMS checks authorization, unwraps the data key, returns the plaintext, and you decrypt locally and wipe the key again. `GenerateDataKeyWithoutPlaintext` is the write-only variant: it returns only the blob, for a component that must create envelopes it will never open itself. ## What the pattern actually buys - **Size and throughput.** The payload can be terabytes; KMS sees only a 32-byte key. - **Cost and quota.** One KMS request per object, not per megabyte. KMS bills per request and enforces a shared per-Region request-rate quota, so this is the difference between a working system and a throttled one. - **Central revocation.** Removing `kms:Decrypt` from a role instantly makes every envelope it could open unreadable, without rewriting a single byte of data. - **Cryptographic erasure.** Scheduling deletion of the KMS key makes every data key wrapped under it permanently unrecoverable. - **Audit.** Every `Decrypt` is a CloudTrail event naming the caller — and if you pass an encryption context, naming which object it was for. ## Where teams get it wrong The classic mistakes are storing the plaintext data key next to the ciphertext (which defeats the entire scheme), keeping the plaintext key alive in a long-running process or letting it reach a log line, and losing the `CiphertextBlob` — without it the object is unrecoverable, because KMS holds nothing that can reconstruct it. At high object rates, calling `GenerateDataKey` per object will eventually hit the Region's request-rate quota; the fix is a bounded data-key cache that reuses one data key for a limited number of messages, bytes, and seconds. The AWS Encryption SDK and the S3 Encryption Client implement this whole flow, including the message format that carries the blob, and are usually a better choice than hand-rolling it.

  • At a few thousand objects a second you start seeing ThrottlingException from KMS. What do you change?
    Cache data keys instead of calling `GenerateDataKey` per object. KMS enforces a shared per-Region request-rate quota across cryptographic operations, so the fix is reuse with explicit bounds — a maximum number of messages, bytes, and seconds per cached key — which trades a wider blast radius per key for a large drop in request volume. Request a quota increase only after caching.
  • What does GenerateDataKeyWithoutPlaintext give you, and when is it the right call?
    It returns only the `CiphertextBlob`. It suits a component that must create envelopes it can never open — an ingest tier that seals records but has no `kms:Decrypt` permission — or a pre-generation step that fetches the plaintext later, at the moment of use, rather than holding it from creation time.
  • Do you need to record which KMS key encrypted an object in order to decrypt it later?
    No. For symmetric keys the key ARN is embedded in the `CiphertextBlob`, so `Decrypt` resolves it on its own. Passing `KeyId` explicitly is still recommended: it makes the call fail loudly if the blob was wrapped by an unexpected key, which blocks an attacker-supplied blob from routing your process to a key they control.

You padlock the crate yourself, then lock the padlock key in the bank's vault and tape the vault's sealed receipt to the crate. The bank never sees the crate, and without vault access the receipt is worthless.

saying these in an interview costs you the question

  • Says KMS stores and encrypts the file itself
  • Stores the plaintext data key beside the ciphertext
  • Calls kms:Encrypt on the whole 500 MB payload
  • Keeps the plaintext data key alive for the process lifetime
  • Thinks losing the CiphertextBlob is recoverable from KMS

context

open as a page

A role in the same AWS account has an IAM policy allowing kms:Decrypt on a customer managed KMS key, but its calls still fail with AccessDenied. Why does KMS behave differently from most AWS resource policies here, and how do you fix it?

level: seniorimportance: must knowfreq 58%

basics

~20 s

Every KMS key has a mandatory key policy, and it is the root of authority for that key. An IAM policy grants nothing unless the key policy also allows the principal, either directly or through its default statement that delegates to IAM.

open as a page

In AWS KMS, what is the practical difference between an AWS owned key, an AWS managed key, and a customer managed key?

level: juniorimportance: should knowfreq 62%

basics

~20 s

They differ in who controls the key policy. AWS owned keys are invisible and shared across customers. AWS managed keys appear in your account under aws/ aliases but their policy is fixed. Only customer managed keys let you set policy, rotation and deletion.

open as a page

You enable automatic key rotation on a customer managed symmetric AWS KMS key. What actually changes, what happens to data already encrypted under it, and when would you instead create a new key and repoint an alias?

level: middleimportance: should knowfreq 48%

basics

~20 s

KMS generates new backing key material and uses it for new encrypt operations. The key ID, ARN and alias are unchanged, old backing keys are retained so existing ciphertext still decrypts, and nothing is re-encrypted. Manual rotation means a new key plus an alias repoint.

open as a page

For a multi-tenant platform on AWS, how would you decide KMS key granularity — one key for the account, one per service, or one per tenant — and what forces the answer?

level: principalimportance: should knowfreq 32%

basics

~20 s

Granularity follows the blast radius and erasure requirements, then is checked against cost and quota. Per-tenant keys buy isolation and cryptographic erasure; a shared key with per-tenant encryption context and grants buys the same authorization boundary far more cheaply.

open as a page

What is the encryption context in an AWS KMS Encrypt or GenerateDataKey request, and what does supplying one give you that the key policy alone does not?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

An encryption context is a set of non-secret key-value pairs bound to the ciphertext. Decrypt must supply exactly the same pairs or it fails, the pairs appear in CloudTrail, and IAM condition keys can scope access by them.

open as a page