Skip to main content
The Credential Vault stores per-user secrets using envelope encryption backed by a managed KMS.

How it works

  • A per-entry data key (DEK) is generated by KMS GenerateDataKey.
  • The value is encrypted locally with AES-256-GCM (Node crypto, never hand-rolled). Blob layout: nonce(12) || tag(16) || ciphertext. No AAD.
  • KMS returns the DEK already wrapped by the shared customer-managed key (CMK). We store ciphertext, wrapped_dek, and kek_id (per row, so rotation knows what wrapped what). The plaintext DEK is zeroized immediately and never persisted.

Per-tenant isolation via EncryptionContext

Every KMS call passes EncryptionContext = { company_id, tenant_user_id }. KMS verifies it on Decrypt, so a wrapped_dek stolen from one tenant’s row cannot decrypt another tenant’s data — even under the same CMK. key (the vault entry name) is deliberately not in the context, so vault keys stay renameable via a plain UPDATE. And there is no AES-GCM AAD — KMS context is the sole crypto binding, avoiding a JSON.stringify canonicalization footgun that would otherwise brick every row on a refactor.

Rotation — two distinct operations

  • Key rotation (POST .../rotate): KMS ReEncrypt re-wraps the DEK under the current CMK version. The value ciphertext is unchanged. Cheap.
  • Value rotation: a normal PUT with a new value (fresh DEK + ciphertext).
  • ?regenerate_dek=true does a full DEK regeneration + value re-encryption.
Automatic key rotation is enabled on the CMK.

IAM — least privilege

No kms:*, no Resource: "*". A request that omits the tenant context is rejected by IAM before it reaches KMS.

Configuration

Env varPurpose
NAIVE_VAULT_KEK_IDThe CMK ARN that wraps every DEK.
NAIVE_KMS_REGIONRegion of the CMK (default us-east-1).
NAIVE_KMS_ACCESS_KEY_IDAccess key for the dedicated KMS IAM user.
NAIVE_KMS_SECRET_ACCESS_KEYSecret for that IAM user.
KMS uses its own credentials — not the AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_REGION env vars. On the hosting platform those are object-storage credentials with region auto; reusing them would route KMS calls to the object store and fail every Vault operation. The dedicated NAIVE_KMS_* trio keeps the two credential sets isolated. When NAIVE_KMS_ACCESS_KEY_ID/NAIVE_KMS_SECRET_ACCESS_KEY are unset (e.g. local dev with a real cloud profile), the client falls back to the default credential chain.

Secrets never travel in URLs

Reveal is a POST (/vault/:key/reveal) — the value comes back in the body, never a query string. List masks values.