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:
| Mode | CustodyMode | Who controls the key |
|---|---|---|
| Platform-managed | platform_managed | Ductor generates and holds the KEK, wrapped under the deployment's master KeyProvider |
| Customer-managed (BYOK) | customer_managed | The KEK is bound to a customer-controlled KMS key reference (AWS KMS / GCP KMS) |
| External vault | external_vault | The 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:
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 toactive).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:
| Scope | Authorizes |
|---|---|
key_custody:read | Describe bindings, list operations, preview a crypto-shred's blast radius — read-only |
key_custody:operate | Provision a KEK (and non-destructive controls) |
key_custody:rotate | Rotate a KEK and invalidate every pod's cache |
key_custody:shred | Stage 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 emittedStaging 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 class | Destroyed 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:
| Scheme | Backend | Notes |
|---|---|---|
awskms | AWS KMS | e.g. awskms://alias/ductor-master?region=us-east-1 |
gcpkms | GCP Cloud KMS | full key resource name |
vault | HashiCorp Vault Transit | e.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
- Hardening — connector credential encryption and how per-tenant KEKs bind into the envelope format.
- Operations → Key management — the separate master-key AEAD rotation runbook.
- Authorization — granting the
key_custody:*scopes.
Tenancy Isolation
How every request is pinned to a tenant, how the token–tenant relationship is resolved, and how the cross-tenant boundary is enforced.
Hardening
Allowed-hosts (DNS-rebinding guard), connector credential encryption and key management, session-secret rules, and a locked-down production checklist.