KMS Backends

Pick a signing backend per Org via a single trait. The KMS layer exposes the KmsClient async trait — four production backends (AWS, GCP, Vault, PKCS#11) plus an in-memory backend for tests — and a KmsKeyProvider adapter that fronts the JWT signer.

URI scheme

Backend selection is declarative — a single tagged enum (KmsConfig) round-trips through YAML or environment, and build_client(&cfg) returns a boxed trait object. The CMEK row stores the same selection as a URI string for human readability.

Scheme Backend Cargo feature Identifier shape
aws-kms: AWS KMS (CMK or alias) aws-kms arn:aws:kms:us-east-1:…:key/…
gcp-kms: GCP Cloud KMS gcp-kms projects/p/locations/l/keyRings/r/cryptoKeys/k/cryptoKeyVersions/1
vault:// HashiCorp Vault Transit vault vault://vault.internal:8200/tetrapus-jwt
pkcs11:// PKCS#11 HSM (Yubikey, CloudHSM, Luna, SoftHSM) pkcs11 pkcs11:///usr/lib/softhsm2.so?slot=0&label=jwt

Status

AWS KMS, GCP Cloud KMS, and Vault Transit are wired and exercised by integration tests. The PKCS#11 client is a stub — the cryptoki module loads and the URI scheme is reserved, but every signing call returns KmsError::Other("pkcs11 sign_es256 not yet implemented"). YubiHSM / CloudHSM bring-up with SoftHSM-in-CI is a tracked follow-up.

Layering

The KMS layer fronts the existing KeyProvider trait that JwtSigner already consumes. Nothing on the auth path knows whether the actual ECDSA operation happens in-process, against an SDK over HTTPS, or inside an HSM.

graph LR JWT["JwtSigner"] --> KP["KeyProvider trait"] KP --> KKP["KmsKeyProvider"] KKP --> KC["KmsClient trait"] KC --> AWS["AWS KMS"] KC --> GCP["GCP Cloud KMS"] KC --> VLT["Vault Transit"] KC --> P11["PKCS#11 HSM (stub)"] KC --> MEM["InMemory (tests)"]

Key rotation

Rotation is a two-state lifecycle: the new key becomes active and starts signing new tokens; the previous key is moved to expired but its public half stays in JWKS for the grace period so in-flight access tokens still verify. list_active_keys() includes both states by contract.

graph LR NEW["new ES256 key"] -->|provision in KMS| ACT["active (signs)"] ACT -->|operator triggers rotate| EXP["expired (verifies only)"] EXP -->|grace window elapses| OUT["pruned from JWKS"]

cmek_bindings table

A KMS selection is stored per-Org in cmek_bindings. The same row holds the URI, the provider-native key id, and the scope the binding applies to (jwt, data, audit). See CMEK / BYOK for the scope semantics.

Column Type Notes
org_idUUIDFK orgs(id), cascade
kms_uriTEXTScheme + endpoint
key_arn_or_idTEXTProvider-native id
scopeTEXTjwt | data | audit
activeBOOLEANDEFAULT TRUE
rotated_atTIMESTAMPTZNULL until first rotation

Related

Questions?

Reach out for help with integration, deployment, or custom domain codecs.