AWS KMS
Use AWS KMS for MainSDK encryption keys - one wrapping key per region, IAM permissions, rotation, main.yaml, local testing and swapping.
AWS Key Management Service (KMS) is the default driver behind keys. MainSDK encrypts each record or file with its own data key, and AWS KMS wraps that data key with a key that never leaves KMS. You give MainSDK one KMS key per region, so a record is always wrapped by its owner's region key. Pick it when your app runs on AWS, or when you have no reason to pick another key service.
Interfaces it serves
Pick AWS KMS in main.yaml for any of these. Your calls stay the same, and you can swap later.
keysBuilding for 0.3Driver aws-kms AWS KMSDefault
keys:
driver: aws-kmsSwap to another provider
Before you start
You need an AWS account with KMS available in the regions you serve.
- Create one symmetric encryption key per MainSDK region. KMS keys are regional. The catalog example uses
us-east-1for US,eu-west-1for EU andap-south-1for India; use the regions your app and data live in. An alias per key (such asalias/pinecart-eu) keepsmain.yamlreadable. - Give your app an IAM role. Prefer workload identity: the role your compute already assumes. KMS credentials must come from workload identity or the
secretsdriver, never from code. - Grant only what envelope encryption needs. Allow the role to generate and decrypt data keys with these keys, in the key policy or an IAM policy (in AWS terms, actions such as
kms:GenerateDataKeyandkms:Decrypt). The exact action list the driver needs is not specified yet. Keep key administration (scheduling deletion, changing policy) with a separate admin role. - Decide on rotation. MainSDK rotates on the
rotateperiod you set. AWS KMS can also rotate key material on its own schedule. How the two interact is not specified yet; avoid turning both on until it is.
Configure
keys:
driver: aws-kms
key: # one wrapping key per region
us: arn:aws:kms:us-east-1:111122223333:alias/pinecart-us
eu: arn:aws:kms:eu-west-1:111122223333:alias/pinecart-eu
in: arn:aws:kms:ap-south-1:111122223333:alias/pinecart-in
per-user: true # a key per person, for crypto-erasure
rotate: 365d # automatic rotation periodRegions are us, eu and in. Keep the local driver for development in an overlay; mainsdk check refuses it in production with MAIN-KEYS-0001.
keys:
driver: localUse it
Most of the time you don't call keys directly: db fields with encrypt: true, files and crypto use it for you. To seal and open a value yourself:
import { auth, keys } from "mainsdk";
export async function POST(req: Request) {
const session = await auth.require(req);
const { note } = await req.json();
const sealed = await keys.encrypt(note, { owner: session.user, region: "eu", label: "phi" });
// { ciphertext, wrappedKey, keyId, alg: "AES-256-GCM" }
const plain = await keys.decrypt(sealed, { purpose: "echo note to owner" });
return Response.json({ ok: plain.byteLength > 0 });
}Erase a person everywhere, backups included, with keys.destroyUser(user) when per-user is on. Rotate on demand with keys.rotate() or keys.rotate({ keyId }).
Local development and testing
- No AWS account: the
localdriver seals and opens values on your machine, for development only. - KMS without AWS: LocalStack emulates KMS, and MainSDK's keys conformance suite runs the
aws-kmsdriver against it. - Tests:
createTestApp()from@mainsdk/testingrunskeyson its in-memory fake, andchaos.on("keys", { outage: true })simulates KMS being unreachable.
import { it } from "node:test";
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";
import { keys } from "mainsdk";
it("keys.AC3 makes a destroyed user's data unreadable", async () => {
createTestApp({ main: "./main.yaml" });
const user = { id: "u1" };
const sealed = await keys.encrypt("note", { owner: user, region: "eu", label: "phi" });
await keys.destroyUser(user);
await assert.rejects(keys.decrypt(sealed, { purpose: "test" }), { code: "MAIN-KEYS-0004" });
});What MainSDK adds on top
- A data key per record (AES-256-GCM or XChaCha20-Poly1305), wrapped by a KMS key that never leaves KMS.
- One wrapping key per region, chosen from the record owner's region.
- Every wrap, unwrap, destroy and rotate written to the audit log with key ID, purpose and caller, never the key material.
- Automatic rotation with old data still readable, rewrapped on its next write or by a background job.
- Per-user keys, so
keys.destroyUser()erases a person's data everywhere.
See Encryption keys for the full contract.
Limits and notes
- The driver comes with the keys spec (phase 1) and is the default for
keys. - A KMS key belongs to one AWS region. Each MainSDK region needs its own key, in an AWS region that fits your data residency.
- Sealing with the label
panis refused, because card numbers are never stored. - Hardware security modules (including AWS CloudHSM) and customer-held keys through AWS XKS come with the hsm-keys spec in phase 3.
keys.raw()returns the KMS client for featureskeysdoes not wrap.mainsdk checklists every use.- On the Fleet plan, MainSDK Cloud can rotate or revoke a key everywhere during an incident; everything above is free in your app.
Swap to or from it
keys:
driver: gcp-kms # was aws-kms; point key.us, key.eu and key.in at the new service's keysFAQ
Can I use one multi-region key for every region? MainSDK asks for one wrapping key per region, set under key.us, key.eu and key.in, so each record stays tied to its owner's region.
Does destroying a user's key delete a KMS key? keys.destroyUser() returns a receipt with the destroyed key ID. How per-user keys map onto KMS resources is not specified yet.
Can I read old data after rotation? Yes. Old versions stay usable for decrypt, and records are rewrapped on their next write.
Next
Open questions
Was this page helpful?