Cloud KMS — Key Management Service

Deep Study: Cryptography Fundamentals, Envelope Encryption, Provider Comparison & Best Practices

AWS KMS GCP Cloud KMS Azure Key Vault HashiCorp Vault Envelope Encryption FIPS 140-2 / HSM

1. Why KMS Exists — The Problem It Solves

Encryption is easy. Key management is hard. A KMS centralizes the lifecycle of cryptographic keys — creation, rotation, access control, auditing, and destruction — so application developers never have to store, rotate, or protect raw key material themselves.

Without KMS — The Problems

Keys live next to data

Encryption key stored in same database / config file as encrypted data. Attacker who reads the DB gets the key too.

No rotation

Rotating keys manually across 20 services is error-prone and rarely done — old compromised keys stay active for years.

No audit trail

You can't answer "who decrypted this data at 3am on Tuesday?" — critical for compliance (PCI-DSS, HIPAA, SOC 2).

Key sprawl

Different teams use different keys with no central inventory. When a breach happens, you don't know what was exposed.

With KMS — The Solutions

Keys never leave the HSM

Raw key material stays inside a Hardware Security Module. Your app sends data to KMS for encryption — the key itself is never transmitted.

Automatic rotation

Enable key rotation with one flag. KMS handles re-wrapping of data keys transparently. Old key versions kept for decryption only.

Every call is logged

CloudTrail / Cloud Audit Logs capture every Encrypt, Decrypt, GenerateDataKey call with who, when, what, from where.

IAM-gated access

Granular permissions: "service A can encrypt, service B can decrypt, only admin can delete". Revoke access instantly.

2. Cryptography Fundamentals You Need

🔑
Symmetric Encryption
AES-256-GCM

Same key encrypts AND decrypts. Very fast — used for bulk data encryption. The challenge: how do you securely share the key?

  • AES-128, AES-256 — standard choices
  • GCM mode = encryption + integrity check
  • Used for: S3 objects, DB columns, disk volumes
🔐
Asymmetric Encryption
RSA / ECC

Public key encrypts, private key decrypts (or vice versa for signing). Slower — used for key exchange and digital signatures.

  • RSA-2048/4096, EC P-256/P-384
  • Used for: TLS, signing JWTs, key wrapping
  • Never use for bulk data — use symmetric instead
✍️
Digital Signatures
Sign & Verify

Private key signs data. Anyone with the public key can verify authenticity. Proves who created the data and that it wasn't tampered with.

  • Used for: code signing, JWT signing, document signing
  • ECDSA (faster), RSA PKCS#1 v1.5, RSA-PSS
🔒
HMAC
Message Authentication

Hash-based Message Authentication Code. Proves integrity + authenticity without public/private key complexity.

  • HMAC-SHA256 most common
  • Used for: API request signing, webhook verification
  • KMS GenerateMac / VerifyMac operations

The Core Principle: Never Encrypt Data Directly with a KMS Key

KMS keys (CMKs/KMS Keys) are not used to encrypt your data directly — they are used to encrypt the data key that encrypts your data. This two-level pattern is called Envelope Encryption and is the most important concept in KMS.

3. Key Hierarchy — Three Levels

Every cloud KMS follows the same three-level hierarchy. Understanding this is the foundation for everything else.

ROOT

Root of Trust / HSM Master Key

Lives INSIDE the Hardware Security Module (HSM). Never exportable. Never visible to anyone — not even cloud provider employees. Derived from true random number generator at HSM initialization. Protects all KMS keys.

Hardware
FIPS 140-2 Level 3
CMK

KMS Key / Customer Master Key (CMK)

Managed by YOU via the KMS console/API. Has ARN/resource ID, IAM policies, usage logs. This key never leaves the KMS — you call KMS to use it, the key bytes never come to your app. Used only to wrap/unwrap Data Keys.

Logical Key
Stays in KMS
DEK

Data Encryption Key (DEK)

A temporary AES-256 key generated by KMS on-demand. This key does leave KMS — it comes to your app to encrypt data. Immediately discarded after use. KMS returns BOTH the plaintext DEK (use once, then discard) AND the encrypted DEK (store alongside your data).

Ephemeral
Comes to your app
DATA

Your Data (Ciphertext)

Encrypted by the plaintext DEK. Stored in S3, RDS, DynamoDB, your database, etc. Alongside it, you store the encrypted DEK (ciphertext). To decrypt: call KMS to decrypt the DEK, then use the DEK to decrypt data, then discard the DEK again.

Your Storage
S3 / DB / disk

4. Envelope Encryption — The Core Pattern

Envelope encryption solves the problem of encrypting large amounts of data without sending all that data through KMS (which would be slow and expensive).

Encrypt Flow

1

Generate Data Key

Call kms.generateDataKey(keyId) → KMS returns:
Plaintext DEK (32 bytes, use once)
Encrypted DEK (wrapped by CMK, store this)

2

Encrypt Data Locally

Use plaintext DEK to encrypt your data with AES-256-GCM. This happens entirely in your application — no round-trip to KMS per record.

3

Discard Plaintext DEK

Zero out the plaintext DEK from memory. Store only: ciphertext data + encrypted DEK side by side.

Decrypt Flow

1

Retrieve Encrypted DEK

Load the encrypted DEK you stored alongside the ciphertext. It's useless without KMS — an attacker who steals your DB only has encrypted data AND an encrypted key.

2

Decrypt the DEK

Call kms.decrypt(encryptedDEK) → KMS checks your IAM permissions, logs the call, returns plaintext DEK. One KMS call per batch, not per record.

3

Decrypt Data Locally + Discard

Use plaintext DEK to decrypt data with AES-256-GCM. Zero out DEK from memory. Your data is now plaintext. KMS was called exactly once, not per record.

ENVELOPE ENCRYPTION — VISUAL FLOW

🔐 KMS Holds CMK HSM-protected Logs every call 💻 Your Application Receives DEK (plaintext) Encrypts data locally Discards plaintext DEK GenerateDataKey plaintext DEK + encrypted DEK Stored Ciphertext AES-256-GCM(data, DEK) your S3 / DB / file Encrypted DEK CMK(DEK) stored alongside ciphertext store store DECRYPT FLOW Load encrypted DEK from storage kms.decrypt(encDEK) → plaintext DEK (1 call per batch) Decrypt data locally AES-GCM decrypt discard DEK from RAM
Why not just encrypt everything directly with the CMK? Two reasons: (1) KMS has throughput limits and per-call pricing — encrypting 1M rows would cost $3 per batch at $0.03/10K calls. With envelope encryption, you call KMS once to get a DEK, then encrypt all 1M rows locally for free. (2) CMKs cannot encrypt more than 4KB of data per call in most KMS APIs.

5. Cloud Providers — Deep Dive

AWS KMS
Key Management Service
Market Leader
  • AES-256, RSA 2048/3072/4096, ECC P-256/P-384/secp256k1, HMAC
  • Customer-managed keys (CMK) vs AWS-managed vs AWS-owned
  • Multi-region keys: replicate key across regions for DR
  • Key aliases: human-readable names (alias/my-key)
  • CloudTrail integration: every KMS call logged automatically
  • Automatic annual rotation (symmetric keys)
  • AWS CloudHSM: dedicated single-tenant HSM hardware
  • External key store (XKS): key material hosted outside AWS
  • Integration: S3, RDS, EBS, Lambda env vars, Secrets Manager, SSM Parameter Store, DynamoDB
# ARN format arn:aws:kms:ap-east-1:123456789:key/abc-def # Pricing (us-east-1) $1.00/month per CMK $0.03 per 10,000 API calls $0.02/month per alias
Google Cloud KMS
Cloud Key Management Service
Versatile
  • Symmetric: AES-128/256 GCM, AES-256 CBC/CTR
  • Asymmetric: RSA 2048/3072/4096, EC P-256/P-384
  • HMAC-SHA256/512
  • Key ring → Key → Key version hierarchy
  • Cloud HSM: FIPS 140-2 Level 3 (same level as AWS)
  • External Key Manager (EKM): keys hosted outside GCP
  • Autokey: automatically creates and manages keys per service
  • Cloud Audit Logs: Data Access logs per key operation
  • Integration: GCS, BigQuery, Cloud SQL, Pub/Sub, Secret Manager
# Resource path format projects/my-proj/locations/asia-east1/ keyRings/my-ring/cryptoKeys/my-key # Pricing $0.06/month per active key version $0.03 per 10,000 operations
Azure Key Vault
+ Managed HSM
Enterprise
  • Keys: RSA 2048/3072/4096, EC P-256/P-256K/P-384/P-521
  • Secrets: arbitrary blobs (connection strings, API keys)
  • Certificates: full X.509 lifecycle management
  • Managed HSM: dedicated single-tenant, FIPS 140-2 Level 3
  • Soft-delete + Purge protection: 7–90 day recovery window
  • RBAC at vault or individual secret/key level
  • Private endpoint: access vault only from inside VNet
  • Integration: Azure Disk Encryption, Azure Storage, SQL TDE, AKS
# URI format https://my-vault.vault.azure.net/ keys/my-key/abc123version # Pricing $0.03 per 10,000 operations (std) $1.00 per 10,000 ops (premium/HSM) Managed HSM: ~$3.20/hour/instance
HashiCorp Vault
Self-hosted / HCP Vault
Self-hosted
  • Transit secrets engine: Encrypt-as-a-Service (no key export)
  • Dynamic secrets: generate DB credentials on demand, auto-expire
  • PKI engine: internal CA, auto-rotating TLS certificates
  • Auth methods: Kubernetes, AWS IAM, OIDC, LDAP, AppRole
  • Kubernetes injector: auto-inject secrets as env vars / files
  • Seal types: auto-unseal with AWS KMS / GCP KMS / Azure Key Vault
  • Audit log: every operation logged to file / syslog / socket
  • Namespaces (Enterprise): multi-team isolation
# Enable transit engine vault secrets enable transit vault write transit/keys/my-key type=aes256-gcm96 # Encrypt without key ever leaving Vault vault write transit/encrypt/my-key \ plaintext=$(base64 <<< "my secret data")

6. Key Types & Algorithms

TypeAlgorithmKey SizeUse CaseSupported By
Symmetric AES-GCM 256-bit Encrypt/decrypt data (envelope encryption) AWS GCP Azure Vault
Asymmetric Encrypt RSA-OAEP 2048 / 4096 Encrypt small data, key wrapping AWS GCP Azure
Asymmetric Sign RSA-PSS / ECDSA P-256 / P-384 Code signing, JWT signing, document integrity AWS GCP Azure
HMAC HMAC-SHA256/512 256–512 bit Message authentication, token signing AWS GCP
ECC secp256k1 256-bit Blockchain / Bitcoin-compatible signing AWS GCP

7. Core KMS Operations — Code Examples

import { KMSClient, GenerateDataKeyCommand, DecryptCommand, EncryptCommand } from "@aws-sdk/client-kms"; import { createCipheriv, createDecipheriv, randomBytes } from "crypto"; const kms = new KMSClient({ region: "ap-southeast-1" }); const KEY_ID = "arn:aws:kms:ap-southeast-1:123456789:key/abc-def"; // ─── ENCRYPT DATA (envelope encryption) ─────────────────────── async function encryptData(plaintext: Buffer): Promise<{ ciphertext: Buffer; encryptedDEK: Buffer }> { // 1. Ask KMS to generate a data key const { Plaintext: dek, CiphertextBlob: encryptedDEK } = await kms.send( new GenerateDataKeyCommand({ KeyId: KEY_ID, KeySpec: "AES_256" }) ); try { // 2. Encrypt locally with the plaintext DEK (never leaves your process) const iv = randomBytes(12); // 96-bit IV for GCM const cipher = createCipheriv("aes-256-gcm", dek!, iv); const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]); const authTag = cipher.getAuthTag(); // integrity check // Ciphertext format: [IV (12)] + [AuthTag (16)] + [EncryptedData] const ciphertext = Buffer.concat([iv, authTag, encrypted]); return { ciphertext, encryptedDEK: Buffer.from(encryptedDEK!) }; } finally { // 3. Zero out plaintext DEK from memory immediately dek?.fill(0); } } // ─── DECRYPT DATA ───────────────────────────────────────────── async function decryptData(ciphertext: Buffer, encryptedDEK: Buffer): Promise<Buffer> { // 1. Ask KMS to decrypt the data key (one call per batch) const { Plaintext: dek } = await kms.send( new DecryptCommand({ CiphertextBlob: encryptedDEK }) ); try { // 2. Decrypt locally const iv = ciphertext.subarray(0, 12); const authTag = ciphertext.subarray(12, 28); const encrypted = ciphertext.subarray(28); const decipher = createDecipheriv("aes-256-gcm", dek!, iv); decipher.setAuthTag(authTag); // verify integrity return Buffer.concat([decipher.update(encrypted), decipher.final()]); } finally { dek?.fill(0); // always zero out DEK } }
import { KeyManagementServiceClient } from "@google-cloud/kms"; const client = new KeyManagementServiceClient(); const keyName = client.cryptoKeyPath( "my-project", "asia-east1", "my-key-ring", "my-key" ); // ─── ENCRYPT (KMS encrypts small data directly, max 64KB) ────── async function encryptSmallData(plaintext: string) { const [result] = await client.encrypt({ name: keyName, plaintext: Buffer.from(plaintext), // Optional: additional authenticated data for integrity additionalAuthenticatedData: Buffer.from("context-string"), }); return result.ciphertext; // store this } // ─── DECRYPT ─────────────────────────────────────────────────── async function decryptData(ciphertext: Uint8Array) { const [result] = await client.decrypt({ name: keyName, ciphertext, additionalAuthenticatedData: Buffer.from("context-string"), }); return result.plaintext?.toString(); } // ─── ASYMMETRIC SIGN (e.g. JWT signing) ─────────────────────── async function signData(message: Buffer) { const hash = createHash("sha256").update(message).digest(); const [signResponse] = await client.asymmetricSign({ name: keyName + "/cryptoKeyVersions/1", digest: { sha256: hash }, }); return signResponse.signature; }
// HashiCorp Vault — Transit Encrypt-as-a-Service // Your app NEVER sees the key. Vault handles all crypto. import axios from "axios"; const VAULT_ADDR = "https://vault.internal:8200"; const VAULT_TOKEN = process.env.VAULT_TOKEN; // from K8s auth const KEY_NAME = "my-app-key"; // ─── ENCRYPT ────────────────────────────────────────────────── async function vaultEncrypt(plaintext: string): Promise<string> { const b64 = Buffer.from(plaintext).toString("base64"); const res = await axios.post( `${VAULT_ADDR}/v1/transit/encrypt/${KEY_NAME}`, { plaintext: b64 }, { headers: { "X-Vault-Token": VAULT_TOKEN } } ); return res.data.data.ciphertext; // "vault:v1:abc123..." } // ─── DECRYPT ────────────────────────────────────────────────── async function vaultDecrypt(ciphertext: string): Promise<string> { const res = await axios.post( `${VAULT_ADDR}/v1/transit/decrypt/${KEY_NAME}`, { ciphertext }, { headers: { "X-Vault-Token": VAULT_TOKEN } } ); return Buffer.from(res.data.data.plaintext, "base64").toString(); } // ─── DYNAMIC SECRET (DB credential) ─────────────────────────── async function getDatabaseCredential() { const res = await axios.get( `${VAULT_ADDR}/v1/database/creds/my-role`, { headers: { "X-Vault-Token": VAULT_TOKEN } } ); // Returns { username: "v-app-abc", password: "xyz", lease_duration: 3600 } // Credential auto-expires after 1 hour. No static passwords anywhere. return res.data.data; }

8. Native Cloud Integrations

Cloud services natively integrate with KMS. You don't write encryption code — you just point the service at a KMS key.

🪣
S3

Server-side encryption (SSE-KMS). Per-object or per-bucket. Each object gets unique DEK wrapped by your CMK. Bucket policies can deny non-KMS uploads.

🗄️
RDS / Aurora

Storage encryption at rest. Select CMK at DB creation. Automated backups and snapshots inherit encryption. Cannot encrypt existing unencrypted DB in-place.

💾
EBS / EC2

Volume encryption. CMK wraps the volume DEK stored in the EBS service. Can set account-level default encryption for all new EBS volumes.

🔒
Secrets Manager

Stores secrets encrypted by KMS CMK. Automatic rotation of DB credentials, API keys. Pull secrets at runtime — no hardcoding. $0.40/secret/month.

λ
Lambda Env Vars

Encrypt Lambda environment variables with CMK. KMS decrypts at cold start. Use KMS instead of plaintext env vars for API keys / DB passwords.

📦
DynamoDB

Table encryption at rest with CMK. DynamoDB-owned key (free), AWS-managed ($0), or CMK ($1/month). Encryption is transparent to all API calls.

🪣
Cloud Storage

Customer-managed encryption keys (CMEK) at bucket or object level. Default encryption with Google-managed keys is free; CMEK uses Cloud KMS pricing.

🗄️
Cloud SQL / Spanner

CMEK for database encryption at rest. Specify key during instance creation. Replicas inherit the key. Key disablement immediately blocks all DB operations.

📊
BigQuery

CMEK for dataset and table encryption. Can also use column-level encryption with AEAD functions referencing Cloud KMS keys in queries.

🔒
Secret Manager

GCP equivalent of AWS Secrets Manager. Stores secrets encrypted with Cloud KMS. IAM-gated access, version history, automatic secret rotation (via Cloud Functions).

💾
Azure Disk Encryption

Encrypts OS and data disks using BitLocker (Windows) or DM-Crypt (Linux). Keys stored in Key Vault. Enables on running VMs without restart.

🗄️
Azure SQL / Cosmos DB

Transparent Data Encryption (TDE) with customer-managed keys in Key Vault. Key Vault Managed HSM for highest compliance requirement. Cannot change key after creation.

☁️
Azure Storage

Customer-managed keys for Blob, Files, Queue, Table. Key rotation automatically re-encrypts storage service encryption keys (not the data) — zero downtime.

⚙️
AKS / App Service

AKS: etcd encryption with Key Vault keys, CSI Secrets Store driver mounts Key Vault secrets as volumes. App Service: reference Key Vault secrets in app settings.

9. Key Rotation — How It Actually Works

What rotates and what doesn't

New key material is generated

KMS generates new cryptographic key material for the CMK. The old material is NOT deleted — it's kept for decryption only.

New encryptions use new material

Any new Encrypt or GenerateDataKey call uses the new key material automatically. No code changes needed.

!

Old ciphertext still decryptable

KMS metadata in the ciphertext blob identifies which key version encrypted it. Old data decrypts with old material, new data with new. Transparent to your app.

Data is NOT re-encrypted automatically

Your S3 objects/DB records still use their old DEKs (wrapped by old key material). You must re-encrypt data yourself if you want forward secrecy at the data layer.

Rotation settings

# AWS — enable automatic rotation aws kms enable-key-rotation \ --key-id alias/my-key # Rotates every year automatically # AWS — manual rotation (on-demand, AWS SDK v3+) aws kms rotate-key-on-demand \ --key-id alias/my-key # GCP — automatic rotation gcloud kms keys update my-key \ --location asia-east1 \ --keyring my-ring \ --rotation-period 90d \ --next-rotation-time 2026-07-01T00:00:00Z # Azure — set key expiry + rotation policy az keyvault key set-policy \ --vault-name my-vault \ --name my-key \ --expires 2027-01-01T00:00:00Z # HashiCorp Vault — rotate transit key vault write -f transit/keys/my-key/rotate # min_decryption_version controls which old # versions can still decrypt (key trimming)
The rotation gap problem: If a CMK is rotated but existing DEKs are not re-wrapped, an attacker who obtains old encrypted DEKs and compromises the old key material can still decrypt old data. True forward secrecy requires periodic re-encryption of all data with new DEKs.

10. HSM & FIPS 140-2 — Hardware Security

🔩
What is an HSM?

Hardware Security Module — a physical computing device dedicated to managing cryptographic keys and performing crypto operations. Tamper-resistant: physically destroys key material if someone tries to open it.

  • True hardware random number generator (TRNG)
  • Cryptographic operations happen inside the chip
  • Cannot extract keys even with physical access
  • FIPS 140-2 Level 3 = highest standard for cloud HSMs
🏅
FIPS 140-2 Levels

Federal Information Processing Standard for cryptographic modules. 4 security levels:

  • Level 1: Software only, basic requirements
  • Level 2: Physical tamper evidence (seals)
  • Level 3: Tamper resistance + key destruction on breach ← cloud HSMs
  • Level 4: Complete physical security envelope
🖥️
Shared vs Dedicated HSM

Standard KMS (AWS, GCP, Azure) uses multi-tenant shared HSMs — your key stays isolated but hardware is shared.

  • AWS CloudHSM: single-tenant HSM cluster (~$1.60/hour)
  • Azure Managed HSM: single-tenant (~$3.20/hour)
  • GCP Cloud HSM: per-key HSM protection (no dedicated hardware)
  • Required for: PCI-DSS Level 1, some government regulations
📋
When You Need Dedicated HSM
  • PCI-DSS Level 1 (card brand mandate)
  • Government / defense (FedRAMP High, DoD IL4+)
  • BYOK with proof key never touched cloud provider
  • eIDAS / qualified electronic signatures (EU)
  • Own crypto officer and HSM administration controls

11. Common Usage Patterns

Encrypt sensitive columns (PII, PAN, SSN) with field-level encryption. Store encrypted DEK per record for key isolation.

// One DEK per user — revoking access to one user's CMK-scope // doesn't expose other users' data // Schema CREATE TABLE users ( id UUID PRIMARY KEY, email TEXT, -- plaintext for lookups ssn_cipher BYTEA, -- AES-256-GCM(ssn) dek_cipher BYTEA, -- KMS_CMK(DEK) — store with row iv BYTEA -- 12-byte GCM IV ); // Insert encrypted row async function insertUser(email: string, ssn: string) { const { ciphertext, encryptedDEK } = await encryptData(Buffer.from(ssn)); await db.query( `INSERT INTO users (email, ssn_cipher, dek_cipher) VALUES ($1, $2, $3)`, [email, ciphertext, encryptedDEK] ); } // Read and decrypt async function getSSN(userId: string): Promise<string> { const row = await db.query(`SELECT ssn_cipher, dek_cipher FROM users WHERE id=$1`, [userId]); const plaintext = await decryptData(row.ssn_cipher, row.dek_cipher); return plaintext.toString(); }

Kubernetes encrypts Secrets at rest in etcd using a KMS plugin (envelope encryption via external KMS).

# /etc/kubernetes/encryption-config.yaml apiVersion: apiserver.config.k8s.io/v1 kind: EncryptionConfiguration resources: - resources: ["secrets"] providers: - kms: name: aws-encryption-provider endpoint: "unix:///var/run/kmsplugin/socket.sock" cachesize: 1000 timeout: 3s - identity: {} # fallback for existing unencrypted secrets --- # External Secrets Operator — pull from AWS Secrets Manager apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: my-app-secrets spec: refreshInterval: 1h secretStoreRef: name: aws-secrets-manager kind: ClusterSecretStore target: name: my-app-secrets # creates K8s Secret with this name data: - secretKey: DB_PASSWORD remoteRef: key: "prod/myapp/db" property: "password"
// Sign JWTs using AWS KMS asymmetric key (no private key in your app) import { KMSClient, SignCommand, GetPublicKeyCommand } from "@aws-sdk/client-kms"; import { createHash } from "crypto"; const KMS_KEY_ID = "arn:aws:kms:...:key/..."; // RSA or ECC key // Sign JWT header + payload async function signJWT(header: object, payload: object): Promise<string> { const headerB64 = Buffer.from(JSON.stringify(header)).toString("base64url"); const payloadB64 = Buffer.from(JSON.stringify(payload)).toString("base64url"); const message = `${headerB64}.${payloadB64}`; // KMS signs the hash — private key never leaves KMS const hash = createHash("sha256").update(message).digest(); const { Signature } = await kms.send(new SignCommand({ KeyId: KMS_KEY_ID, Message: hash, MessageType: "DIGEST", SigningAlgorithm: "RSASSA_PKCS1_V1_5_SHA_256", })); return `${message}.${Buffer.from(Signature!).toString("base64url")}`; } // Verify: fetch public key from KMS once, cache it, verify locally async function getPublicKey() { const { PublicKey } = await kms.send(new GetPublicKeyCommand({ KeyId: KMS_KEY_ID })); return PublicKey; // DER-encoded — can be shared publicly } // Verification is pure crypto, no KMS call needed
# S3 bucket policy — deny uploads without KMS encryption { "Version": "2012-10-17", "Statement": [{ "Sid": "DenyNonKMSUploads", "Effect": "Deny", "Principal": "*", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::my-bucket/*", "Condition": { "StringNotEquals": { "s3:x-amz-server-side-encryption": "aws:kms" } } }] } // SDK: upload with SSE-KMS const { S3Client, PutObjectCommand } = require("@aws-sdk/client-s3"); await s3.send(new PutObjectCommand({ Bucket: "my-bucket", Key: "sensitive/file.pdf", Body: fileBuffer, ServerSideEncryption: "aws:kms", SSEKMSKeyId: "alias/my-key", })); // S3 handles the envelope encryption internally // Downloads auto-decrypt if caller has kms:Decrypt permission

12. Provider Comparison

Feature AWS KMS GCP Cloud KMS Azure Key Vault HashiCorp Vault
Deployment AWS-managed GCP-managed Azure-managed Self-hosted / HCP
FIPS Level Level 2 (std) / Level 3 (HSM) Level 3 (Cloud HSM) Level 3 (Premium/Managed) Depends on backend
Multi-cloud AWS-only GCP-only Azure-only Any cloud / on-prem
Secrets engine Secrets Manager (separate) Secret Manager (separate) Key Vault Secrets (built-in) Built-in (KV, dynamic)
Dynamic secrets Via Secrets Manager rotation Via Secret Manager + Cloud Functions Via Key Vault + Functions Native (DB, cloud, SSH)
Audit logging CloudTrail (all calls) Cloud Audit Logs Azure Monitor Diagnostics Audit log to file/syslog
Key pricing $1/key/month + calls $0.06/version/month + calls $0/key (std vault fee) + ops Open source = infra cost
External key material XKS (bring your own) EKM BYOK Full control
Best for AWS workloads, deep S3/RDS integration GCP-native, BigQuery encryption Azure AD integration, certificates Multi-cloud, Kubernetes, on-prem

13. Best Practices

Key Design

1
One CMK per service / data classification, not one global key for everything
2
Use key aliases in code, not key ARNs/IDs — makes rotation easier without code changes
3
Enable automatic key rotation (annual minimum, 90-day for high-sensitivity data)
4
Tag keys with: service=checkout, env=prod, data-class=pci for cost allocation and audit
5
Set deletion waiting period to maximum (30 days AWS) — accidental deletion = permanent data loss

Access Control

1
Least privilege: app can kms:GenerateDataKey + kms:Decrypt only, not kms:DeleteKey
2
Separate encrypt-only and decrypt-only roles — a compromised write path can't decrypt old data
3
Use kms:ViaService condition to restrict key use to specific AWS services only
4
Enable MFA for kms:ScheduleKeyDeletion and kms:DisableKey
5
Monitor: CloudWatch alarm on KMSInvalidKeyState and unexpected decrypt volumes

Application Patterns

1
Always zero out plaintext DEK from memory after use (buffer.fill(0))
2
Cache decrypted DEKs in process memory for batch operations — one KMS call per file, not per row
3
Use AES-256-GCM (not CBC) — provides both encryption AND integrity/authentication in one pass
4
Store IV/nonce with ciphertext — a new unique IV per encryption is required for GCM security
5
Implement exponential backoff for KMS API calls — enforce retry on ThrottlingException

The Mental Model: KMS is a Key Custodian, Not an Encryption Service

Think of KMS like a bank vault for keys, not a processing plant for data. You don't bring all your data to the bank to encrypt it — you go to the bank to get a locked box (encrypted DEK), take it home, unlock it (decrypt DEK via KMS), use what's inside (encrypt/decrypt data locally), put the box back empty, and return the locked box to storage. The bank never sees your data. You never see the master key. The audit log shows every time you visited the bank. That's envelope encryption.

Common Mistakes to Avoid:
• Storing plaintext secrets in environment variables instead of Secrets Manager / Key Vault
• Using the same CMK for all environments (dev key compromise shouldn't endanger prod)
• Not setting up CloudTrail alerting on unexpected KMS decrypt activity (potential exfiltration indicator)
• Encrypting with CBC mode instead of GCM — CBC has no integrity check, vulnerable to padding oracle attacks
• Re-using the same IV/nonce with the same key — breaks GCM security completely