Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

Rotate a virtual key with zero downtime

Roll a virtual key to a successor without dropping traffic — both resolve through a grace window, and the ledger records the lineage.

How-to guideLast updated

On this page

Roll a compromised or aging virtual key to a fresh successor without dropping a single request: tokentriage keys rotate mints a successor and keeps the predecessor valid through a grace window, so callers migrate on their own schedule and the ledger records exactly which key served each row.

Prerequisites

  • A running TokenTriage deployment with governance enabled and at least one active ttv_ key. See Virtual keys.
  • Shell access to a host that can reach governance.db (by default under the --state-dir, which also holds audit.jsonl).
  • The kid of the key you are rotating. It is the vk_-prefixed public id, not the secret. List it if you don’t have it:
$ tokentriage keys list
KID             STATE    OWNER                    STUB     EXPIRES              LABEL
vk_FEKZdMs2UQRi active   org:acme/team:ml/user:dev1 EeNp     2026-11-07T20:41:14Z dev1-laptop

Steps

1. Rotate with a grace window

Pass the predecessor’s kid and a --grace duration. --grace is a Go duration string (24h, 30m, 168h) — during that window both the predecessor and the successor resolve, so in-flight callers keep working while you distribute the new secret.

$ tokentriage keys rotate vk_FEKZdMs2UQRi --grace 24h
rotated vk_FEKZdMs2UQRi -> vk_xXxNzkjKvCAX (predecessor valid for 24h0m0s)
kid:     vk_xXxNzkjKvCAX
This secret is shown ONCE and cannot be recovered — copy it now:
ttv_FodkC2K2Vozp…

You should see a rotated … -> … line naming the predecessor and the new successor kid, followed by the shown-once secret block. The successor inherits the predecessor’s owner, scopes, label, dims policy, and metadata, so you do not re-specify them.

Note that the command does not print an expires: line even when the successor inherits a real expiry from its predecessor. keys list in the next step shows the truth.

2. Distribute the new secret before the window closes

Update your callers to present the new ttv_… secret. The predecessor keeps resolving until now + grace, and no longer than its own pre-existing expiry, so a rotation cannot extend a key’s life. Past that deadline the gate stops accepting it.

Confirm both states while the window is open:

$ tokentriage keys list
KID             STATE    OWNER                    STUB     EXPIRES              LABEL
vk_FEKZdMs2UQRi active   org:acme/team:ml/user:dev1 EeNp     2026-08-10T20:47:20Z dev1-laptop
vk_xXxNzkjKvCAX active   org:acme/team:ml/user:dev1 Fodk     2026-11-07T20:41:14Z dev1-laptop

Read that table across: both rows say active, the predecessor’s EXPIRES was rewritten to the grace deadline, and the successor kept the original expiry. That is the window.

3. (Optional) Revoke immediately for a compromised key

If the key is compromised and you cannot tolerate any overlap, pass --grace 0 (the default). The successor is minted and the predecessor is revoked immediately — the next snapshot swap misses it.

$ tokentriage keys rotate vk_TJjLgu3CXQw6 --grace 0
rotated vk_TJjLgu3CXQw6 -> vk_oXrW71or9o3n (predecessor revoked immediately)
kid:     vk_oXrW71or9o3n
This secret is shown ONCE and cannot be recovered — copy it now:
ttv_X_r7trsW-iiV…

What the ledger records

Both secrets work during the window, and the rows say which one served each request. A request presenting the new secret carries serving_key.rotated_from naming the predecessor, plus a rotation_grace marker that stays true only while the predecessor is still valid. A request presenting the old secret is attributed to the predecessor’s own kid, with no lineage and no marker, because that key was not rotated from anything.

So “how many callers have migrated?” is a question the ledger answers directly, by counting rows per serving_key.kid.

Your budgets do not double. The successor inherits the predecessor’s owner node, so both secrets resolve to the same tenancy node and the same limit instances; a grace window widens who can call, not what they may spend.

{
  "decision": {
    "governance": {
      "rotation_grace": true,
      "serving_key": {
        "kid": "vk_xXxNzkjKvCAX",
        "node": "org:acme/team:ml/user:dev1",
        "rotated_from": "vk_FEKZdMs2UQRi"
      }
    }
  }
}

The lineage names public kids and no secret material. Keys are stored as SHA-256 hashes, and the ttv_ prefix is stripped before any upstream forward, so a provider does not see a TokenTriage key. Every rotation is also written to the durable audit log with actor cli.

One flag matters if you run more than one replica. Point --config at a config whose shared_state.postgres tier is enabled and the rotation also writes to the cross-replica revocation mirror, so the successor and the predecessor’s new state reach the other replicas within the published propagation bound. Without --config the command stays local and opens no connection. If Postgres is configured but unreachable, the rotation still commits locally, warns on stderr that it was not propagated, and does not retry, so re-run it when Postgres is back.