seekrit
Docs/AWS KMS drop-in

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.

note

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 idbilling, 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

OperationNotes
Encrypt / DecryptAES-256-GCM; encryption context bound as AAD
GenerateDataKey / …WithoutPlaintextAES_256, AES_128, or NumberOfBytes
DescribeKey, ListKeysmetadata for the keys the token can see
GenerateRandomCSPRNG bytes
caution

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.