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:
- Access to Vastralaya's specific CMK key ARN
- 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_tenantthat 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.