# KMS key-policy templates (2026)

Two patterns. The first is the **shared classification CMK with per-tenant encryption-context
isolation** that the post recommends as the default. The second is the per-tenant fallback for when
a contract names per-tenant keys.

All policies assume `123456789012` is the account and `r-platform-data` is the role that performs
crypto operations. Replace before use. Key policies are evaluated *in addition to* IAM — keep the
root statement so the account never loses administrative access to the key.

## Pattern 1 — shared classification CMK, tenant isolation via encryption context

The application MUST set `EncryptionContext={"tenant": "<tenant-id>"}` on every Encrypt /
GenerateDataKey / Decrypt call. A tenant role can only act on its own ciphertext because the
condition pins the context value.

```json
{
  "Version": "2012-10-17",
  "Id": "classification-pii-key",
  "Statement": [
    {
      "Sid": "AccountAdmin",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::123456789012:root" },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "AppUsesKeyWithTenantContext",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::123456789012:role/r-platform-data" },
      "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:GenerateDataKey",
        "kms:GenerateDataKeyWithoutPlaintext",
        "kms:ReEncryptFrom",
        "kms:ReEncryptTo",
        "kms:DescribeKey"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "s3.us-east-1.amazonaws.com"
        },
        "Null": {
          "kms:EncryptionContext:tenant": "false"
        }
      }
    }
  ]
}
```

> Enforce the context value per tenant in the *IAM role* (not the key policy) so you do not rewrite
> the key policy for every new tenant:
> `"StringEquals": { "kms:EncryptionContext:tenant": "${aws:PrincipalTag/tenant}" }`.

## Pattern 2 — per-tenant CMK (use only when contractually required)

```json
{
  "Version": "2012-10-17",
  "Id": "tenant-acme-key",
  "Statement": [
    {
      "Sid": "AccountAdmin",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::123456789012:root" },
      "Action": "kms:*",
      "Resource": "*"
    },
    {
      "Sid": "OnlyAcmeRoleUsesThisKey",
      "Effect": "Allow",
      "Principal": { "AWS": "arn:aws:iam::123456789012:role/r-tenant-acme" },
      "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:GenerateDataKey",
        "kms:DescribeKey"
      ],
      "Resource": "*"
    }
  ]
}
```

Budget before you ship Pattern 2: every key is **$1/month** existence + up to **$2/month** for the
first two rotations + per-request charges, and all keys still share the same account+Region request
quota. At 3,000 tenants that is ~$3,000/month in *key storage alone* before a single decrypt.

## Cut request cost on either pattern

- **S3 Bucket Keys**: turn on for every SSE-KMS bucket. Cuts KMS request volume up to **99%** by
  reducing per-object `GenerateDataKey` calls to per-bucket-key calls.
- **Data key caching** (AWS Encryption SDK): reuse a data key across many objects; cuts
  `GenerateDataKey` rate (the operation most likely to throttle) without changing the trust model
  if you bound cache lifetime and message count.
