AWS KMS drop-in
seekrit-kms is a small gateway that speaks the AWS KMS JSON API on a local
endpoint. Point an AWS SDK's KMS endpoint at it and your existing KMS code works
unchanged — but every operation runs in the gateway process, against a
managed key fetched as a wrapped grant and unwrapped
locally. The seekrit API never sees your plaintext, your data keys, or the key
material. It's a drop-in KMS that your provider literally cannot read.
This is the envelope-encryption surface of KMS — the ~90% of real usage: S3 and
DynamoDB client-side encryption, Tink, and any GenerateDataKey pattern.
Asymmetric Sign/Verify, GenerateMac, and key-policy management are not
part of this gateway today.
Run the gateway
Give it a service token that holds grants on the managed keys you want to use. It binds loopback by default.
export SEEKRIT_TOKEN=skt_…
seekrit-kms # listening on 127.0.0.1:9911
As a container (bind 0.0.0.0 so the workload can reach it):
docker run --rm \
-e SEEKRIT_TOKEN=skt_… -e SEEKRIT_KMS_LISTEN=0.0.0.0:9911 \
-p 9911:9911 seekritdev/kms
Startup is fail-closed: a missing or bad token, or an unreachable API, stops it from starting rather than serving a broken endpoint.
Point your SDK at it
Only the endpoint changes. The SDK still signs requests with its own credentials (dummy values are fine) — the gateway ignores the signature; the service token is the real authority.
# AWS CLI
export AWS_ENDPOINT_URL_KMS=http://127.0.0.1:9911
export AWS_ACCESS_KEY_ID=ignored AWS_SECRET_ACCESS_KEY=ignored AWS_REGION=us-east-1
aws kms generate-data-key --key-id billing --key-spec AES_256
# boto3
import boto3
kms = boto3.client("kms", endpoint_url="http://127.0.0.1:9911")
# Envelope encryption:
dk = kms.generate_data_key(KeyId="billing", KeySpec="AES_256")
data_key = dk["Plaintext"] # use locally, then discard
wrapped = dk["CiphertextBlob"] # store beside your ciphertext
# ...later...
data_key = kms.decrypt(CiphertextBlob=wrapped)["Plaintext"]
# Direct encrypt/decrypt:
ct = kms.encrypt(KeyId="billing", Plaintext=b"4111 1111 1111 1111")["CiphertextBlob"]
pt = kms.decrypt(CiphertextBlob=ct)["Plaintext"]
// @aws-sdk/client-kms (v3)
import { KMSClient, GenerateDataKeyCommand } from "@aws-sdk/client-kms";
const kms = new KMSClient({ endpoint: "http://127.0.0.1:9911", region: "us-east-1" });
const dk = await kms.send(new GenerateDataKeyCommand({ KeyId: "billing", KeySpec: "AES_256" }));
Referring to keys
A KMS KeyId resolves to a managed key by name or id — billing,
alias/billing, the kms_… id, or an ARN the gateway returned all map to the
same key. Create keys the usual way (seekrit kms create --name billing --purpose encrypt, or the dashboard) and grant the gateway's token access to them.
Encryption context
AWS EncryptionContext is bound as additional authenticated data, exactly like
KMS: Decrypt must supply the same context or it fails. It's canonicalized
order-independently, so {tenant, field} and {field, tenant} are equivalent.
Supported operations
| Operation | Notes |
|---|---|
Encrypt / Decrypt | AES-256-GCM; encryption context bound as AAD |
GenerateDataKey / …WithoutPlaintext | AES_256, AES_128, or NumberOfBytes |
DescribeKey, ListKeys | metadata for the keys the token can see |
GenerateRandom | CSPRNG bytes |
You can't take over ciphertext that real AWS KMS produced. The gateway holds seekrit keys, not your AWS keys — so it's a drop-in for new usage or a re-encrypt migration, not a transparent takeover of an existing encrypted corpus. Ciphertext is interchangeable only within seekrit.
Security
The gateway is a plaintext boundary, like the agent egress proxy and the Kubernetes sidecar: it holds a token and produces data keys and plaintext. It performs no AWS SigV4 verification, so treat reachability to its port as access to everything the token can decrypt — keep it on loopback or a trusted local network, scoped to a token that grants only the keys that workload needs. A disabled key stops working through the gateway within one refresh interval.