CoursesAWS security engineeringKMS & envelope encryption

KMS & envelope encryption

Data keys, key policies, rotation, and the kill switch.

Advanced35 min · lesson 7 of 15

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.

envelope encryption: one key, two forms
$ 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.

read it back, and encrypt a small secret directly
$ 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.

key policy: managing the key and using it are different jobs
{
"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 analyst
An error occurred (AccessDeniedException) when calling the Decrypt operation:
User: arn:aws:sts::222222222222:assumed-role/analyst/quarterly is not authorized to
perform: kms:Decrypt on this resource because no key policy, grant, or identity-based
policy 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.)

a grant is narrow, temporary, and revocable on its own
$ 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.

The kill switch cuts both ways
Disabling or deleting a KMS key locks out an attacker instantly, and it locks out every one of your own services just as instantly. If a single key protects twenty applications, that's your blast radius: pull it to contain an incident and all twenty go dark. Deletion is worse, because you can't un-press it. KMS forces you to schedule it with a waiting period of 7 to 30 days, and once that window closes the key is gone for good, along with every byte it ever wrapped. Scope keys per data domain so the blast radius is a decision you made on purpose, and never schedule deletion on a key still guarding live data.
Where each piece actually lives
your storage (the locked box)
encrypted payload
any size, encrypted locally on your machine
wrapped data key
the sealed copy, stored right beside it
KMS (the bank vault)
KMS key (the master key)
the master key, never leaves KMS
unwrap on request
returns the data key only if you're allowed
the access decision
key policy
who may use the key, standing
grant
narrow, temporary, revocable on its own
Bulk data and the wrapped key sit in your storage; only the master key lives in KMS. Lose the wrapped key and the data is gone even though KMS is perfectly healthy.
Quick check
01A nightly ETL job needs Decrypt on one KMS key for tonight's run only, and you must not permanently widen the key policy or the job's IAM policy. Best mechanism?
Correct — A grant is narrow, carries an encryption-context constraint, and revokes on its own without touching the key policy or IAM policy. Exactly the temporary-access tool.
Incorrect — That's standing access, not temporary, and it edits the very key policy you were told to leave alone.
Incorrect — A wildcard gives the job decrypt on every key in the account, far more than one key for one night.
Incorrect — Disabling is a global kill switch that also blocks every other service using the key; it doesn't scope access to the ETL job at all.
02In envelope encryption, calling generate-data-key against KMS (AWS Key Management Service) returns both a Plaintext key and a CiphertextBlob. How are the two meant to be used to protect a terabyte of data?
Incorrect — KMS never touches your bulk data; only the small key travels to it, which is the entire point of envelope encryption.
Incorrect — you wipe the plaintext and keep the wrapped blob; storing the plaintext next to the data defeats the encryption.
Incorrect — at read time you send only the CiphertextBlob, and KMS unwraps it back to the plaintext data key if you're allowed.
Correct — you lock the data with the plaintext key on your own machine, discard the plaintext, and keep the sealed copy next to the file so KMS can unwrap it later.
03A service wrapped a data key with encryption context project=reports. Later a job calls kms:Decrypt on that blob with correct key permissions but passes encryption context project=analytics, and the call fails. Why?
Correct — the lesson describes the encryption context as a label baked into the seal that decrypt has to match, otherwise KMS won't open it even with the right key access.
Incorrect — you may pass a context; it simply has to match the one used at encryption, so a matching label would succeed.
Incorrect — rotation keeps every old wrapped key decryptable against the old backing material, so rotation is not what breaks this.
Incorrect — the failure is the context mismatch, not Region; the self-describing blob carries the key id, but the context still has to match.

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.

Related