Rotation and versioning
Every crypto key in KnoxCall is versioned. New encryptions always use the latest active version; old ciphertext keeps decrypting against whatever version originally produced it. Rotation moves the active pointer forward without breaking anything that’s already encrypted.The version lifecycle
active— used for new encryptions. Exactly one per key at any time.retired— kept around so old ciphertext under that version still decrypts. As many as you’ve rotated.destroyed— cryptographic erasure. The wrapped key bytes are wiped and the row stays as an audit marker. Ciphertext under a destroyed version is permanently unreadable.
Rotating a key
- A new 256-bit key is generated and wrapped with the tenant master key.
- The new version row is inserted as
active. - The previous active version is flipped to
retired. - The key’s
current_versionpointer is updated.
encrypt calls from this point forward emit knoxcall:v2:... ciphertext. decrypt calls handle either v1 or v2 ciphertext seamlessly.
When to rotate
Rotating is cheap. Decrypting from old versions is also cheap. The only meaningful “cost” of holding many retired versions is audit-trail clutter.
Rewrap: migrate ciphertext to a new version
Rotating doesn’t re-encrypt existing data. Old ciphertext keeps decrypting against retired versions. To migrate it:rewrap decrypts the old ciphertext with the retired version, re-encrypts with the active version, and returns the new ciphertext — the plaintext never crosses the wire. The operation runs entirely on KnoxCall’s side.
The typical migration loop:
Destroying a version (cryptographic erasure)
- Refuses with
409 deletion_not_allowedifdeletion_allowed = falseon the parent key (the safety latch — see below for how to raise it). - Refuses with
409 active_versionif the version isactive— rotate first. - Wipes the wrapped key bytes from the row, flips status to
destroyed, records who and when.
deletion_allowed
Every key ships with deletion_allowed = false. Flipping it to true is a deliberate, audit-logged action that says “I’m about to destroy a version on purpose.”
Flip it from the API:
/v1 call is bounded by the credential’s transit:manage grant instead, because there is no human on the machine plane to re-authenticate.
Pattern we recommend:
- Rewrap all data to the new active version.
- Confirm in a query that no row in your DB still has ciphertext at the version you’re about to destroy.
PATCH /v1/crypto/keys/<name>with{"deletion_allowed": true}.DELETE /versions/<old>.PATCHit back to{"deletion_allowed": false}.
transit.key.deletion_allowed audit row recording the previous and new value, so “who armed this key, and when” is answerable after the fact.
Before 2026-09, this latch had no writer on any surface: it was
false for every key, the
admin UI’s Destroy button was therefore never shown, and a DELETE of any version answered
500. Both are fixed — the refusal is now a 409 with a
deletion_not_allowed code, and the latch is settable on both surfaces as above.What rotation doesn’t do
- It doesn’t break existing decryption — old ciphertext keeps working until you destroy the old version.
- It doesn’t re-encrypt existing data — that’s
rewrap. - It doesn’t change the ciphertext format — same prefix, just a higher version number.
- It doesn’t propagate to bundled-mode agents instantly — they pick up the new version on their next session-bundle refresh.
Key cache behaviour
Unwrapped key versions are cached in memory for 5 minutes after first use. Rotation invalidates the previous active entry; destroy invalidates the destroyed entry. You don’t need to worry about the cache day-to-day — it’s an internal performance optimisation, not something you manage.Audit log markers
See also
- Crypto Keys overview →
- Tenant master key → — the wrap chain that protects every transit key