Auth & Security

Key custody & crypto-shred (BYOK)

Per-tenant key-encryption-key custody — platform-managed vs BYOK modes, the binding lifecycle, the staged crypto-shred state machine, and pluggable KMS backends.

Key custody is the control plane for each tenant's key-encryption key (KEK) — the key that wraps the data keys protecting that tenant's ciphertext. It is where bring-your-own-key (BYOK) and crypto-shred live: a customer can bind their tenant to a KMS key they control, and an operator can render a tenant's ciphertext permanently unreadable by destroying its KEK. This is a distinct concern from the deployment-wide master-key AEAD rotation runbook in Operations → Key management; that rotates the master key, this governs per-tenant KEKs.

Custody modes

A tenant's KEK is held under one of three custody modes:

ModeCustodyModeWho controls the key
Platform-managedplatform_managedDuctor generates and holds the KEK, wrapped under the deployment's master KeyProvider
Customer-managed (BYOK)customer_managedThe KEK is bound to a customer-controlled KMS key reference (AWS KMS / GCP KMS)
External vaultexternal_vaultThe KEK is wrapped through an external HashiCorp Vault Transit key the customer administers

Only the coarse, non-secret classifiers are stored in the clear on the binding record — the provider scheme, and SHA-256 prefixes of the provider URI and the backend key reference (which may embed account identifiers). The wrapped KEK bytes are never stored plaintext; a KEKFingerprint (a SHA-256 prefix) identifies the active key without exposing any bytes.

The binding lifecycle

Every tenant has a TenantKeyBinding whose Status moves through a fixed set of states. A RecordVersion compare-and-swap fence guards every mutation, so concurrent operations cannot corrupt the state:

provision rotate disable crypto-shred new gen live re-enable finalize unprovisioned active rotation_pending disabled shred_pending shredded
  • active — a live wrapped KEK backs the tenant's v2 ciphertext.
  • rotation_pending — one immutable next generation is fenced while the current generation stays readable.
  • disabled — new tenant-scoped encrypt work is blocked, but the KEK still resolves so existing ciphertext decrypts (an emergency pause; re-enable is the only ordinary way back to active).
  • shred_pending — a crypto-shred is staged/approved but not finalized; the KEK still resolves until finalize.
  • shredded — the KEK row is soft-deleted and every pod evicted; the tenant's v2 ciphertext is permanently unreadable.

Escalating scopes

Operations are gated by four escalating scopes, weakest to strongest. Each names exactly what it authorizes:

ScopeAuthorizes
key_custody:readDescribe bindings, list operations, preview a crypto-shred's blast radius — read-only
key_custody:operateProvision a KEK (and non-destructive controls)
key_custody:rotateRotate a KEK and invalidate every pod's cache
key_custody:shredStage and finalize the irreversible crypto-shred

`key_custody:shred` is the strongest scope for a reason

Shred is the only operation that can render ciphertext permanently unreadable. It is gated by its own dedicated scope — holding key_custody:rotate or key_custody:operate does not grant it. Reserve key_custody:shred for the smallest possible set of principals, and see Authorization for how scopes are granted.

Crypto-shred is staged, not a single call

Destroying a tenant's KEK is irreversible, so it is deliberately not a single call. It runs as a CAS-guarded state machine with distinct, append-only lifecycle events:

preview   ── honest blast radius; touches no key material

stage     ── validate approval_ref + confirmation_token (== tenant_id);
   │         capture coverage snapshot; quiesce tenant → shred_pending

  shred_started

  shred_quiesced        new tenant-scoped work blocked

finalize  ── soft-delete KEK; invalidate all pods

  shred_invalidated     KEK destroyed, pods evicted

  shred_verified        v2 decrypt now fails (verified live; fail-closed)

  shred_finalized       receipt emitted

Staging and finalizing are two separate calls, and finalize demands an approval reference plus a confirmation token that must equal the tenant id — a guard against a fat-finger shred of the wrong tenant. After the KEK is destroyed the service verifies that v2 decrypt actually fails: if a decrypt still succeeds, the operation is not reported as verified — the fail-closed alternative to a false "done."

Ciphertext survives honestly

Not all of a tenant's ciphertext is under its KEK. Envelope classes distinguish what a tenant crypto-shred does and does not destroy:

Envelope classDestroyed by tenant crypto-shred?
tenant_kek_v2 (per-tenant DEK ciphertext)Yes — this is what a shred renders unreadable
master_key_v1 / master_key_v0 (master-key ciphertext)No — survives a tenant shred

Every shred preview and receipt reports both numbers: how many blobs a shred would (or did) render unreadable, and how many master-key blobs survive. A non-zero surviving count is surfaced as an explicit warning so no operator can mistake a tenant crypto-shred for total destruction of every trace of a tenant's data.

Pluggable KEK backends

The backend that wraps and unwraps KEK material is selected by URI scheme, and each self-registers:

SchemeBackendNotes
awskmsAWS KMSe.g. awskms://alias/ductor-master?region=us-east-1
gcpkmsGCP Cloud KMSfull key resource name
vaultHashiCorp Vault Transite.g. vault://host:8200/transit/keys/ductor — mount defaults to transit

Every backend call is wrapped in a gobreaker circuit breaker, so a KMS or Vault outage trips open and fails fast rather than hanging every request behind it.

Key bytes are a typed value the linters guard

Plaintext key bytes flow as the named type KeyMaterial (from domain/keyprovider). It is deliberately a distinct type — audit linters flag any attempt to log or persist a KeyMaterial value, so short-lived plaintext key bytes cannot leak into a log line or a database column by accident. This is the same discipline the redacted binding record follows: fingerprints and URI hashes are stored, never the underlying secrets.

Where to go next