Skip to content

Key rotation

Rotating a key gives it a new version and makes that version the one all new ciphertext is made under. Rotation is manual: a key rotates only when you call POST /v1/keys/{id}/rotate or press Rotate in the dashboard, and rypt never rotates a key by itself. Each tier keeps a fixed number of versions. A rotation that takes a key past that number schedules its oldest version for destruction, and ciphertext under that version stops decrypting at once. On Free (free) and Extra wrapped (wrapped) keys, that happens on every rotation.

A rotation, in this order:

  1. creates a new version of the key and makes it the primary version, so that from then on encrypt, wrap and rewrap make ciphertext under it;
  2. if the key now has more enabled versions than its tier keeps, schedules the oldest for destruction until it has that many.

The key keeps its id, name, tier and container. Ciphertext names the version it was made under, so decrypt, unwrap and rewrap keep working on ciphertext under any version the key still keeps. On this page, ciphertext includes wrapped data keys.

A version stops working as soon as the rotation schedules it, before the call returns. A decrypt, unwrap or rewrap of a ciphertext under a scheduled version is refused with 400 invalid_ciphertext:

{
  "error": "invalid_ciphertext",
  "message": "ciphertext refers to a key version that is not enabled",
  "request_id": "5b0e7c1a-9d3f-4e2b-8a6c-1f4d2e3b5a79"
}

Its key material is destroyed 24 hours after it is scheduled. On Free and Extra wrapped keys, database backups can still hold it, wrapped, until they expire, and unwrapped copies can remain in rypt's freed memory; see storage and plaintext in memory. Those 24 hours are not a grace period for you. Only the operator can restore a version during them; there is no API call or dashboard control for it, and rypt makes no promise to do it.

You cannot choose which version encrypt or wrap uses, disable or re-enable a version, or change how many versions a tier keeps. A key's tier is fixed when it is created.

Tier Versions kept What a rotation schedules for destruction Made before the rotation and still decrypts
Free (free) 1 The version that was primary until then, every time Nothing
Extra wrapped (wrapped) 1 The version that was primary until then, every time Nothing
Software (software) 3 Nothing if the key had fewer than 3 versions; otherwise its oldest Ciphertext under the 2 versions before the new one
Hardware (hsm) 2 Nothing if the key had 1 version; otherwise its oldest Ciphertext under the version before the new one

The table is what a rotation does when the one before it succeeded. After a failed rotation, the next one can schedule more; see if a rotation fails.

A new key has one version. Counting from the moment it is made, a ciphertext on a Software key still decrypts after the next two rotations and stops at the third; on a Hardware key it stops at the second; on a Free or Extra wrapped key, at the next. rewrap moves it onto the primary version, which starts that count again.

curl -sS -X POST "https://api.rypt.dev/v1/keys/$KEY_ID/rotate" \
  -H "Authorization: Bearer $RYPT_TOKEN"

It takes no request body and answers 200 with the key. For a Software key that had versions 1, 2 and 3:

{
  "id": "0f1e2d3c-4b5a-6978-8796-a5b4c3d2e1f0",
  "container_id": "3e2d1c0b-9a87-4654-b321-0fedcba98765",
  "name": "prod-database",
  "tier": "software",
  "versions": ["2", "3", "4"],
  "primary_version": "4",
  "created_at": "2026-09-16T10:00:00.123456Z",
  "state": "active",
  "delete_requested_at": null,
  "destroy_after": null
}

versions lists the key's enabled versions, oldest first, and primary_version is the one new ciphertext is made under. Version 1 was scheduled for destruction, which the response does not say: compare versions with what GET /v1/keys/{id} returned before the rotation.

  • A rotation is not an operation. It does not count toward the key's monthly operations, does not reset that count, is not billed, and is never refused for the Free cap or for billing.
  • An API key needs the rotate action; see access control. A dashboard session needs no action.
  • A key pending deletion is refused with 409 key_pending_deletion. Restore it first, with POST /v1/keys/{id}/restore (an API key needs the restore action) or Restore in the dashboard.
  • A successful rotation writes a rotate row to the key's audit log, with the new primary version as kms_version. A rotation that fails after rypt has found the key, or that is refused by the API key's key list or actions, writes one without a version.
  • On Software and Hardware keys, a rotation waits for Cloud KMS to generate the new version's key material, for up to 60 seconds.
Response Cause
401 unauthorized The credential is missing or invalid.
403 ip_not_allowed The API key has a firewall, and the request came from outside it.
403 key_not_allowed, 403 action_not_allowed The API key's key list or actions do not allow it. See access control.
404 not_found The id is not a UUID, was never issued, names a destroyed key, or names a key in another container or tenant.
409 key_pending_deletion The key is pending deletion.
429 rate_limited The rate limit. See limits.
503 unavailable rypt could not verify the API key or check its actions, or no slot came free within 30 seconds because too many key changes were in flight (too many key lifecycle operations in flight; retry shortly). Nothing changed, and a retry is safe.
500 internal_error Cloud KMS or rypt failed, before, part-way through or after the rotation took effect. See below.

A 500 does not tell you how far the rotation got. It can come before anything changed; after the new version was created, or made primary, or older versions were scheduled for destruction; or after the whole rotation succeeded and only its audit row could not be written. If you get no answer at all, assume the same.

rypt's own record of the key's versions, which GET /v1/keys/{id} and the dashboard read, is written only when a rotation finishes. Until the next successful rotation, it can show versions and primary_version as they were before, including a version that has already been scheduled and no longer decrypts. If the key still shows the versions it had before, that does not tell you whether the rotation took effect. If it shows a new primary_version, the rotation finished.

encrypt, wrap and rewrap use the primary version the key really has, which can already be the new one while GET still shows the old one. Their audit rows name the version they used as kms_version.

The next rotation that succeeds counts every enabled version that exists by then, and puts the record right. It can therefore schedule more versions than one rotation would: on a Hardware key, that can include the version you have just rewrapped onto. Before you rotate again, decrypt or unwrap whatever you cannot afford to lose, as on a one-version key.

Which version a ciphertext is under

Section titled "Which version a ciphertext is under"

No call reports it. The decrypt and unwrap responses do not say which version opened a ciphertext, and rypt keeps no copy of the ciphertext it returns, so it cannot list what is still under an old version. Before a rotation retires a version, assume that anything you hold under the key may be under it. If you want to track versions yourself, the audit log row of each encrypt, wrap and rewrap names the version it used, under the same request id as the call's x-request-id response header.

rewrap moves ciphertext to the primary version

Section titled "rewrap moves ciphertext to the primary version"

rewrap is how you move ciphertext from an older version of a key to its primary. rewrap shows the call, how to send a wrapped data key, and why the aad cannot change. For rotation, what matters:

  • It always re-encrypts under the key's current primary version, whatever version the ciphertext is under, the primary included.
  • It takes one ciphertext per call, and each call is one operation. There is no batch call and no call that rewraps everything under a version.
  • It works within one key. It cannot move ciphertext to another key.
  • It opens the old ciphertext first, so once that ciphertext's version is scheduled for destruction it fails with 400 invalid_ciphertext. It moves ciphertext off a version only before the rotation that retires it.
  • On a paid key whose billing is not in good standing it is 402 billing_required (rotation is not), and on a Free key whose monthly cap is used up it is 429 ops_cap_reached, like every operation.

If you have ciphertext you still need, this is the only order that keeps it:

  1. decrypt every ciphertext, and unwrap every wrapped data key, that you still need from this key. Keep the results somewhere safe until step 3 is done.
  2. Rotate, once.
  3. encrypt each value again, and wrap each data key again by sending it as dek (see wrap your own data key). Store the new ciphertext and wrapped data keys in place of the old ones. Data you encrypted under a data key does not change; only its wrapped data key is replaced.

Send the same aad in step 3 as in step 1, unless you mean to change it.

This costs two operations per value. On a Free key both count toward its 10,000 operations a month, and at the cap decrypt and unwrap are refused too, so check how many operations the key has left before step 1. Limits explains the cap.

The API reference advises "Rewrap first, then rotate." That holds only for Software and Hardware keys. On Free and Extra wrapped keys it saves nothing.

The alternative is not to rotate. Create a second key, encrypt new data under it, and keep this one for as long as its ciphertext matters. A tenant can hold only one Free key that has not been destroyed, so on Free the second key has to be on a paid tier. To move existing data across, decrypt under the old key and encrypt under the new one.

If you expect to rotate a key while its ciphertext is still in use, create it as Software or Hardware from the start.

These keys keep three and two versions, so existing ciphertext survives a rotation as long as you move it onto the primary version before its own version is scheduled:

  1. Read the key with GET /v1/keys/{id}. If versions already holds 3 entries on a Software key, or 2 on a Hardware key, the rotation will schedule the first one. rewrap anything that may still be under it now: that moves it to the current primary version, which the rotation keeps. If a rotation of this key has failed since the last one that succeeded, versions may be out of date; see if a rotation fails.
  2. Rotate.
  3. rewrap every ciphertext and wrapped data key you hold under the key, and store the results in place of the old values. Finish before you rotate the key again.

If you finish step 3 after every rotation, nothing is left under the oldest version when the next rotation comes, and step 1 has nothing to do.

  1. In the dashboard, open Encryption keys, then the key.

  2. Press Rotate, in the key's header. It is there on every tab while the key is active; a key pending deletion has no Rotate.

  3. The dialog, Rotate "{name}", reads the key again and shows one of three cases:

    • A Software or Hardware key below its limit: how many of its versions it has, and that nothing already encrypted is affected.
    • A Software or Hardware key at its limit: the version the rotation will destroy, and a rewrap snippet with Copy snippet, to run first.
    • A Free or Extra wrapped key: that every existing ciphertext stops decrypting, that rewrap does not help, the decrypt, rotate, re-encrypt order above, and a decrypt snippet.

    In the last two cases, type the key's name in "Type the key's name to rotate it" before Rotate can be pressed.

  4. Press Rotate.

The snippets are for your terminal: decrypt and rewrap need an API key, and the dashboard cannot run them. A key's Versions tab lists its enabled versions, newest first, marked Primary or Retained. Its Overview tab shows the Primary version and Enabled versions.

The dialog reads the same record as GET /v1/keys/{id}, so after a failed rotation the version it says it will destroy can be wrong. If a rotation fails with internal_error, the dialog's "The rotation did not happen." may not be true; see if a rotation fails. The dashboard does not repeat a failed rotation by itself, and pressing Rotate again starts a new one.