KMS & envelope encryption
Data keys, key policies, rotation, and the kill switch.
Encryption at rest sounds like a solved problem until you ask one blunt question: if you're holding a terabyte of customer data, do you really ship all of it off to some service to get it encrypted? You don't, and you can't. AWS Key Management Service (KMS for short) never touches your bulk data. It looks after small keys and hands them out under tight access control, and that's the whole job. The trick that lets one small key protect a whole terabyte is called envelope encryption. Once that clicks, the parts that sound scary, like rotating a key or cutting off someone's access, stop being mysterious.
The box and the bank vault
Here's the shape of it. You've got documents to protect, so you lock them in a strong box, and the box takes a small key. Now you have a second problem: where do you keep that small key so nobody can grab it? You lock it inside a bank vault. Map that onto AWS. The documents are your data. The box is ordinary encryption you run yourself, on your own machine. The small key is a data key. The bank vault is KMS, holding a master key that never leaves the building.
When you ask KMS for a data key, it hands you the same key in two forms at once. One is the plaintext key, the real metal key you use right now to lock the box. The other is a sealed copy that only the vault can open, called the ciphertext blob. You lock your data with the plaintext key, wipe that plaintext key from memory, and keep the sealed copy stored next to the encrypted file. Your terabyte never travels to KMS. Only the tiny key does.
$ aws kms generate-data-key \--key-id alias/reports-cmk \--key-spec AES_256 \--encryption-context project=reports{"CiphertextBlob": "AQIDAHi9xk2pQ7v3Rm8...c1a...(~200 base64 chars)...Zq2a9XwK0uY","Plaintext": "aFq7Kd2u9wQ0aXcVbNm3lPzR7sT1yE4hG8jF5oI6uY0=","KeyId": "arn:aws:kms:eu-west-1:222222222222:key/8d9f2b7e-4c3a-4e1f-9b2c-7a6d5e4f3c1a"}
Two fields do the work. Plaintext is the raw AES-256 key (a standard, industrial-strength encryption key) written in base64; you feed it straight into your crypto library, or a tool like openssl, to encrypt the payload, then throw it away. CiphertextBlob is that same key already wrapped by your KMS key. You store the blob beside the ciphertext and forget about it until read time. The blob is bigger than the key it wraps because it carries the key id and some metadata, so later on it can describe itself.
Reading it back, and the shortcut for small things
To decrypt, you hand the sealed copy back to the vault. Send the CiphertextBlob to KMS, and if your identity is allowed, KMS unwraps it and returns the plaintext data key. You unlock the box, read your documents, and drop the key again. You never tell KMS which key wrapped this blob, because the blob already says so. For a small secret like a database password or an API token (a short secret string an app uses to log in), anything under four kilobytes, you can skip the envelope entirely and let KMS encrypt and decrypt it directly. Go bigger than that and KMS refuses the plaintext, which is its way of telling you to use a data key.
$ aws kms encrypt --key-id alias/reports-cmk \--plaintext fileb://db-password.txt \--encryption-context project=reports{"CiphertextBlob": "AQICAHg8m2pR7v0aXcVbNm3lPzR...Kx9Q==","KeyId": "arn:aws:kms:eu-west-1:222222222222:key/8d9f2b7e-4c3a-4e1f-9b2c-7a6d5e4f3c1a","EncryptionAlgorithm": "SYMMETRIC_DEFAULT"}$ aws kms decrypt --ciphertext-blob fileb://wrapped.key \--encryption-context project=reports{"KeyId": "arn:aws:kms:eu-west-1:222222222222:key/8d9f2b7e-4c3a-4e1f-9b2c-7a6d5e4f3c1a","Plaintext": "aFq7Kd2u9wQ0aXcVbNm3lPzR7sT1yE4hG8jF5oI6uY0=","EncryptionAlgorithm": "SYMMETRIC_DEFAULT"}
Notice the second call returns the KeyId even though you never passed one. A normal KMS blob, the symmetric kind where the same key both locks and unlocks, is self-describing, so KMS reads the key id straight out of it and unwraps against the right material. That's also why losing the blob is fatal, it's the only pointer back to the key. The encryption context (project=reports) is a label baked into the seal. Decrypt has to present the same label or KMS refuses to open it, so it's a cheap way to bind a wrapped key to the job it belongs to.
Who can actually decrypt: key policies and grants
Every KMS key carries a key policy, and that policy is the final say on who's allowed to touch the key. Unlike most AWS permissions, a key policy can stand entirely on its own, without any matching IAM policy (IAM is AWS Identity and Access Management, the account's usual permission system), so it's the one place you can be sure controls the key. The useful move is to split two jobs people tend to lump together. Key administrators can create, describe, enable, disable, and even schedule the key for deletion, but they get no Decrypt. The application can Decrypt and GenerateDataKey but can't manage the key at all. A leaked admin role still can't read your data, and a leaked app role still can't delete your key.
{"Version": "2012-10-17","Statement": [{"Sid": "KeyAdminsManageButCannotDecrypt","Effect": "Allow","Principal": { "AWS": "arn:aws:iam::222222222222:role/kms-admin" },"Action": ["kms:Create*","kms:Describe*","kms:Enable*","kms:Disable*","kms:PutKeyPolicy","kms:ScheduleKeyDeletion"],"Resource": "*"},{"Sid": "AppMayUseButNotManage","Effect": "Allow","Principal": { "AWS": "arn:aws:iam::222222222222:role/report-svc" },"Action": ["kms:Decrypt","kms:GenerateDataKey"],"Resource": "*","Condition": { "StringEquals": { "kms:ViaService": "s3.eu-west-1.amazonaws.com" } }}]}$ aws kms decrypt --ciphertext-blob fileb://wrapped.key --profile analystAn error occurred (AccessDeniedException) when calling the Decrypt operation:User: arn:aws:sts::222222222222:assumed-role/analyst/quarterly is not authorized toperform: kms:Decrypt on this resource because no key policy, grant, or identity-basedpolicy allows the kms:Decrypt action
The kms:ViaService condition tightens it further: report-svc can use the key only through S3 (Amazon's file-storage service, where the encrypted objects live), never by calling Decrypt on its own. That separates reading the store from using the key, which was the whole reason you encrypted in the first place. When you need something narrower and temporary, don't widen the key policy. Create a grant instead.
A grant is a small, revocable permission you pin to a key without editing the key policy or anyone's IAM policy. It spells out who gets in, exactly which operations they can call, and (optionally) a constraint like an encryption context, the same project=reports label the wrapped key was bound to. AWS services quietly create grants for you all the time. Because you can revoke a grant on its own, it's the right tool for 'let tonight's ETL job decrypt this one key' without permanently handing anyone more access. (An ETL job is just a scheduled task that pulls data in, reshapes it, and loads it somewhere else.)
$ aws kms create-grant \--key-id alias/reports-cmk \--grantee-principal arn:aws:iam::222222222222:role/nightly-etl \--operations Decrypt GenerateDataKey \--constraints EncryptionContextSubset={project=reports} \--name etl-2026-07{"GrantId": "0c8e2f4b9d1a6c3e7f2b8a4d5e9c1f0a2b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f","GrantToken": "AQpAY2E1Zm9vYmFy...abbreviated...Zm9vYmFyMTIz"}$ aws kms revoke-grant --key-id alias/reports-cmk \--grant-id 0c8e2f4b9d1a6c3e7f2b8a4d5e9c1f0a2b3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f$ echo $?0
Rotation is nearly free here, and that falls straight out of the design. Because only the little wrapped keys point back at the master, rotating it mints fresh backing material on a schedule while every old wrapped key still decrypts against the old material. No re-encrypting a terabyte. Turn on automatic rotation and set the interval to taste (KMS lets you pick anything from 90 days up, and the default is a year). This same envelope routine is what SSE-KMS, server-side encryption using KMS, does under an S3 bucket: S3 asks KMS for a data key per object, and S3 Bucket Keys reuse one data key across many objects to keep the KMS bill down, which is right where the next lesson picks up.
Try this
Work through “Who can actually decrypt: key policies and grants” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: the kill switch cuts both ways. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.