# Key custody & crypto-shred (BYOK) (/docs/auth/key-custody)



Key custody is the control plane for each tenant's &#x2A;*key-encryption key (KEK)**
— the key that wraps the data keys protecting that tenant's ciphertext. It is
where &#x2A;*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](/docs/operations/key-management); that rotates the
master key, this governs per-tenant KEKs.

## Custody modes [#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 [#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:

```mermaid
stateDiagram-v2
    [*] --> unprovisioned
    unprovisioned --> active: provision
    active --> rotation_pending: rotate
    active --> disabled: disable
    active --> shred_pending: crypto-shred
    rotation_pending --> active: new gen live
    disabled --> active: re-enable
    shred_pending --> shredded: finalize
    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 [#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                                      |

<Callout title="`key_custody:shred` is the strongest scope for a reason" type="warn">
  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](/docs/auth/authorization) for how scopes are granted.
</Callout>

## Crypto-shred is staged, not a single call [#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:

```text
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 [#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 [#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.

<Callout title="Key bytes are a typed value the linters guard" type="info">
  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.
</Callout>

## Where to go next [#where-to-go-next]

* [Hardening](/docs/auth/hardening) — connector credential encryption and how
  per-tenant KEKs bind into the envelope format.
* [Operations → Key management](/docs/operations/key-management) — the separate
  master-key AEAD rotation runbook.
* [Authorization](/docs/auth/authorization) — granting the `key_custody:*`
  scopes.
