Skip to main content

Book 4 · The AI Solutions Architect · 7 min read

AWS KMS Customer Managed Keys for AI Data Isolation

A shared encryption key means a compromised infrastructure can read all tenants' data. Per-tenant CMKs with EncryptionContext bind each ciphertext to its tenant — decryption with the wrong context fails even if both keys are accessible. One key per tenant, automatic annual rotation.

AWS KMS Customer Managed Keys for AI Data Isolation

Encryption at rest protects data from infrastructure compromise. A Customer Managed Key (CMK) via AWS KMS gives each tenant control over their own encryption key — so that even within the same cloud account, one tenant's data cannot be decrypted using another tenant's key.

The Valid Failure

ShopBot's six tenants were on per-tenant Qdrant collections after the Book 4 migration. The data was physically separated. But all six collections were encrypted with the same AWS-managed key — the default encryption applied by the cloud provider.

A compliance officer at one of the enterprise tenants asked: "If your infrastructure is compromised, can you guarantee that our catalog data cannot be read by another tenant's administrator?"

With a shared encryption key: no. An attacker who obtained the shared key could decrypt all six tenants' data. Per-tenant CMKs mean each tenant's data is encrypted with a key that only that tenant (and explicitly granted IAM roles) can use.

What a Customer Managed Key Provides

Encryption type Key holder Who can decrypt
AWS-managed key (default) AWS Anyone with IAM access to the resource
Service-managed CMK Your AWS account Anyone with IAM access to the key + resource
Per-tenant CMK Tenant controls key policy Only roles explicitly granted in key policy

Per-tenant CMK means: to decrypt Vastralaya's Qdrant payload, you need:

  1. Access to Vastralaya's specific CMK key ARN
  2. An IAM role that Vastralaya's key policy allows

No other tenant's CMK can be substituted.

Creating Per-Tenant CMKs

import boto3
import json

kms = boto3.client("kms", region_name="ap-south-1")

KNOWN_TENANTS = [
    "vastralaya", "jaipur_threads", "mumbai_silk",
    "chennai_weaves", "delhi_couture", "pune_fashion"
]

def create_tenant_cmk(tenant_id: str, account_id: str) -> str:
    response = kms.create_key(
        Description=f"ShopBot CMK for tenant: {tenant_id}",
        KeyUsage="ENCRYPT_DECRYPT",
        Origin="AWS_KMS",
        Policy=json.dumps({
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "RootAccess",
                    "Effect": "Allow",
                    "Principal": {"AWS": f"arn:aws:iam::{account_id}:root"},
                    "Action": "kms:*",
                    "Resource": "*",
                },
                {
                    "Sid": f"TenantServiceAccess_{tenant_id}",
                    "Effect": "Allow",
                    "Principal": {
                        "AWS": f"arn:aws:iam::{account_id}:role/shopbot-{tenant_id}-service-role"
                    },
                    "Action": ["kms:Encrypt", "kms:Decrypt", "kms:GenerateDataKey"],
                    "Resource": "*",
                    "Condition": {
                        "StringEquals": {
                            "kms:EncryptionContext:tenant_id": tenant_id
                        }
                    },
                },
            ],
        }),
    )
    key_id = response["KeyMetadata"]["KeyId"]

    # Create alias for readability
    kms.create_alias(
        AliasName=f"alias/shopbot-{tenant_id}",
        TargetKeyId=key_id,
    )
    print(f"Created CMK for {tenant_id}: {key_id}")
    return key_id

# Create one key per tenant
tenant_keys = {}
for tenant in KNOWN_TENANTS:
    tenant_keys[tenant] = create_tenant_cmk(tenant, account_id="123456789012")

Using CMK Encryption in the Ingest Pipeline

import base64

def encrypt_chunk_payload(payload: dict, tenant_id: str) -> dict:
    """Encrypt sensitive payload fields before storing in Qdrant."""
    plaintext = json.dumps(payload).encode()

    response = kms.encrypt(
        KeyId=f"alias/shopbot-{tenant_id}",
        Plaintext=plaintext,
        EncryptionContext={"tenant_id": tenant_id},
    )

    return {
        "encrypted_payload": base64.b64encode(
            response["CiphertextBlob"]
        ).decode(),
        "tenant_id": tenant_id,
        # Store unencrypted search metadata separately
        "product_id": payload.get("product_id"),
        "chunk_type": payload.get("chunk_type"),
    }

def decrypt_chunk_payload(stored_payload: dict) -> dict:
    """Decrypt payload at query time."""
    tenant_id = stored_payload["tenant_id"]
    ciphertext = base64.b64decode(stored_payload["encrypted_payload"])

    response = kms.decrypt(
        CiphertextBlob=ciphertext,
        EncryptionContext={"tenant_id": tenant_id},
    )
    return json.loads(response["Plaintext"])

The EncryptionContext ({"tenant_id": tenant_id}) is what makes the key tenant-specific. A ciphertext encrypted with tenant_id="vastralaya" in the context cannot be decrypted with the Vastralaya key if the decryption request specifies a different tenant_id — even if the same key is used. The context is authenticated.

Key Rotation Policy

AWS KMS supports automatic annual key rotation for CMKs:

kms.enable_key_rotation(KeyId=f"alias/shopbot-{tenant_id}")

When a key rotates, AWS KMS automatically re-encrypts data keys with the new key version. Existing ciphertexts remain decryptable with the rotated key — rotation is transparent to the application. The new key is used for all new encryptions.

For GDPR-relevant data, enable key rotation for all tenant CMKs immediately after creation.

What CMKs Do Not Protect Against

CMK isolation protects against:

  • Infrastructure compromise where an attacker reads raw Qdrant storage
  • Insider access at the cloud provider level
  • Cross-tenant key misuse (EncryptionContext prevents this)

CMK isolation does not protect against:

  • Application-layer bugs (a bug in retrieve_for_tenant that passes the wrong tenant_id)
  • IAM over-permission (a service role granted access to all CMKs)
  • Stolen application credentials that have legitimate decryption permissions

Physical collection isolation + CMK encryption are complementary layers. Neither replaces the other.

The Valid Knowledge

  • EncryptionContext is the binding mechanism: without it, any CMK in your account could decrypt any ciphertext. The context binds the ciphertext to a specific tenant — decryption with the wrong context fails.
  • CMK isolation satisfies enterprise compliance requirements: HIPAA, SOC 2, ISO 27001, and GDPR all recognise cryptographic isolation as a valid control for data separation. Metadata filters do not.
  • Key rotation is automatic and transparent: enable it on creation. A rotating key is materially stronger than a static key and costs nothing extra.
  • Encrypt payload, not vectors: the vector itself contains no PII (it is a mathematical representation of text). Encrypt the payload fields — the product text, customer session data, and any identifiers.
  • IAM key policy is the access control layer: the CMK key policy specifies exactly which IAM roles can encrypt and decrypt. Audit these policies as part of your security review — overly permissive key policies defeat the purpose of per-tenant CMKs.

This is the concept. The book is the system.

Book 4: The AI Solutions Architect

Articles give you understanding. The book gives you a working system — full production code, RAGAS evaluation scores, and the patterns that hold up at 11 PM when something breaks.