AWS KMS (Key Management Service)

7 min read index source

AWS KMS (Key Management Service)

Managed encryption-key service. Stores and controls the keys; most other AWS services (S3, RDS, EBS, Secrets Manager, DynamoDB) integrate with it. The interview centers on envelope encryption and key policies vs IAM.

Envelope encryption — the core concept

KMS doesn’t encrypt your large data directly. Instead:

  1. You ask KMS for a data key (GenerateDataKey). KMS returns it twice: plaintext and encrypted-under-the-CMK.
  2. You encrypt your data locally with the plaintext data key, then discard the plaintext key.
  3. You store the encrypted data key alongside the ciphertext.
  4. To decrypt: send the encrypted data key to KMS (Decrypt), get the plaintext key back, decrypt your data locally, discard the key again.
GenerateDataKey  →  { plaintext_key, encrypted_key }
                       │              │
            encrypt data locally    store with ciphertext

              discard plaintext_key

Decrypt(encrypted_key) → plaintext_key → decrypt data locally → discard

Why this design:

  • KMS never sees your data — only small data keys cross the wire.
  • Throughput — you only call KMS once per data key, not once per byte. KMS has request limits; envelope encryption keeps you under them.
  • The CMK never leaves KMS — it’s the root of trust; it can’t be exported.

GenerateDataKey vs Encrypt: Encrypt encrypts data directly with the CMK — only for small payloads (≤4 KB) like a password or another key. GenerateDataKey is the envelope-encryption primitive for everything bigger.

CMK types

Type Who manages it When to use
AWS-owned AWS, shared across accounts, invisible to you default encryption for some services; you can’t see or control it
AWS-managed (aws/s3, aws/rds, …) AWS, one per service per account, visible default when you “enable encryption” on a service without specifying a key; can’t customize the policy or rotation
Customer-managed (CMK) you when you need control: custom key policy, rotation control, cross-account access, audit, the ability to disable/delete

For anything where you need to control who can decrypt or audit decryption, use a customer-managed key. AWS-managed keys are fine for “encrypt at rest, don’t care about fine-grained control.”

Key policies vs IAM vs grants

This trips people up. Every KMS key has a key policy — and unlike most resources, the key policy is the primary access control, not IAM.

  • Key policy — a resource policy on the key. The root authority. If the key policy doesn’t allow a principal (directly or by delegating to IAM), no IAM policy can grant access.
  • IAM policy — grants KMS permissions to a principal, but only takes effect if the key policy delegates to IAM (the standard "Principal": {"AWS": "arn:aws:iam::ACCOUNT:root"} statement that says “IAM policies in this account can govern this key”).
  • Grants — programmatic, temporary, fine-grained delegations. A service (or your code) can create a grant allowing another principal specific operations on the key, often scoped with encryption context. Used heavily by AWS services internally and for “let this Lambda decrypt, just for this”.

The evaluation: a request is allowed only if both the key policy (possibly via IAM delegation or a grant) and the principal’s IAM policy allow it. Explicit deny anywhere wins.

// Minimal key policy delegating to IAM + allowing a specific role
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnableIAM",
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::123456789012:root"},
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "AllowAppDecrypt",
      "Effect": "Allow",
      "Principal": {"AWS": "arn:aws:iam::123456789012:role/my-app"},
      "Action": ["kms:Decrypt", "kms:GenerateDataKey"],
      "Resource": "*"
    }
  ]
}

Encryption context

An optional set of key-value pairs passed to Encrypt/GenerateDataKey. It’s:

  • Cryptographically bound to the ciphertext — Decrypt must be given the same context or it fails.
  • Logged in CloudTrail — gives you audit detail (“decrypted for tenant=acme”).
  • Usable in key-policy / grant conditions"kms:EncryptionContext:tenant": "acme" scopes a grant to one tenant.

Use it for multi-tenant isolation and audit.

Rotation

  • Automatic rotation — for customer-managed keys, AWS rotates the backing key material yearly (or a configurable period). The key ID stays the same; old ciphertext is still decryptable (KMS keeps old versions). Free to enable, transparent.
  • Manual rotation — create a new key, re-encrypt, update references. Needed when you want a new key identity (e.g., after a suspected compromise) — automatic rotation keeps the same key ID.
  • AWS-managed keys rotate automatically (yearly), not configurable.

Multi-region keys

A KMS key that exists as related replicas in multiple regions, sharing key material. Ciphertext encrypted in one region can be decrypted in another. Use for: cross-region DR, global tables, replicating encrypted data. Without multi-region keys, ciphertext is region-locked to its key.

Direct use vs implicit use

You interact with KMS two ways:

Mode Example You call KMS?
Implicit enable SSE-KMS on an S3 bucket; RDS encryption at rest; encrypted EBS volume; Secrets Manager no — the service calls KMS for you (GenerateDataKey/Decrypt under the hood)
Direct your app calls kms:GenerateDataKey to envelope-encrypt a file, or kms:Decrypt to read a secret you encrypted yourself yes — your code makes the API calls

Most of the time you use KMS implicitly. Direct use is for application-level encryption of data the AWS-managed integrations don’t cover.

import boto3
kms = boto3.client("kms")

# Envelope-encrypt a payload
resp = kms.generate_data_key(KeyId="alias/my-app", KeySpec="AES_256")
plaintext_key = resp["Plaintext"]
encrypted_key = resp["CiphertextBlob"]
# ... encrypt data with plaintext_key locally (e.g., AES-GCM), discard plaintext_key ...
# store encrypted_key + ciphertext together

# Later: decrypt
plaintext_key = kms.decrypt(CiphertextBlob=encrypted_key)["Plaintext"]
# ... decrypt data locally, discard plaintext_key ...

Cost model

  • Per customer-managed key: ~$1/month.
  • Per request: ~$0.03 per 10,000 API calls (Encrypt, Decrypt, GenerateDataKey, …).
  • AWS-managed keys: free for the key itself; you still pay per request.

The gotcha: a hot code path calling kms:Decrypt on every request racks up request charges and can hit KMS rate limits. Envelope encryption + data-key caching (decrypt the data key once, reuse it for many operations) is the fix — see the AWS Encryption SDK’s caching support.

KMS in cross-account access

To let account B decrypt data encrypted by account A’s key:

  1. Account A’s key policy must allow account B’s principal.
  2. Account B’s principal needs an IAM policy allowing kms:Decrypt on account A’s key ARN.

Both sides, as always. Encryption context conditions can scope it further.

Common gotchas

  • Forgetting the key policy delegates to IAM — a new customer-managed key created via CLI without the "Principal": {"AWS": "...:root"} statement is unusable by IAM policies; you can lock yourself out.
  • Encrypt for large data — 4 KB limit. Use GenerateDataKey + local encryption (envelope encryption).
  • KMS request throttling in hot paths — cache data keys; don’t call Decrypt per request.
  • Region-locked ciphertext — a single-region key’s ciphertext can’t be decrypted in another region. Use multi-region keys if you need that.
  • Deleting a key is irreversible (after the 7-30 day waiting period) — and all ciphertext encrypted under it becomes permanently unrecoverable. Disable first; delete only when certain.
  • kms:Decrypt permission needed everywhere the data is read — Secrets Manager, encrypted S3 objects, encrypted SQS — the consuming role needs kms:Decrypt on the relevant key, not just the service permission.

Interview angle

  • “Explain envelope encryption.” — KMS generates a data key, returns it plaintext + encrypted-under-the-CMK. You encrypt data locally with the plaintext key, discard it, store the encrypted key with the ciphertext. To decrypt, send the encrypted key to KMS for the plaintext back. Keeps your data out of KMS, keeps you under KMS rate limits, never exports the CMK.
  • GenerateDataKey vs Encrypt?”Encrypt encrypts small payloads (≤4 KB) directly with the CMK. GenerateDataKey is the envelope-encryption primitive for anything larger — it gives you a data key to encrypt with locally.
  • “AWS-managed vs customer-managed keys?” — AWS-managed: zero config, no policy/rotation control, can’t audit fine-grained. Customer-managed: you control the key policy, rotation, cross-account access, can disable/delete, full CloudTrail audit. Use customer-managed when you need control or audit.
  • “Key policy vs IAM policy?” — the key policy is the root authority on a KMS key. IAM policies only work if the key policy delegates to IAM (the root principal statement). A request needs both the key policy (or a grant) and the IAM policy to allow it.
  • “Does using SSE-KMS on S3 mean I call KMS?” — no, that’s implicit use — S3 calls KMS for you. Direct use is when your application code calls GenerateDataKey/Decrypt for application-level encryption.
  • “Why is KMS in my hot path expensive?” — per-request charges (~$0.03/10k) plus rate limits. Fix: envelope encryption with data-key caching — decrypt the data key once, reuse it for many operations instead of calling Decrypt per request.
  • “What’s encryption context?” — key-value pairs cryptographically bound to the ciphertext (must match on decrypt), logged in CloudTrail, and usable in key-policy/grant conditions — used for multi-tenant isolation and audit.