Rotate a virtual key with zero downtime
Roll a compromised or aging virtual key to a successor without dropping traffic — old and new both resolve through a grace window, and the ledger records the lineage on serving_key.rotated_from.
Last 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 Governance, virtual keys & budgets. - Shell access to a host that can reach
governance.db(by default under the--state-dir, which also holdsaudit.jsonl). - The kid of the key you are rotating. It is the
vk_-prefixed public id — never the secret. List it if you don’t have it:
$ tokentriage keys list
KID STATE OWNER STUB EXPIRES LABEL
vk_9kQ2mX7tP4aZ active org:acme/team:ml ab12 never ml-serving
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_9kQ2mX7tP4aZ --grace 24h
rotated vk_9kQ2mX7tP4aZ -> vk_5rD8nB1uL0cQ (predecessor valid for 24h0m0s)
kid: vk_5rD8nB1uL0cQ
This secret is shown ONCE and cannot be recovered — copy it now:
ttv_…
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 — you do not re-specify them.
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 (never longer than its own pre-existing expiry — rotation never extends a key’s life), then goes inactive. A background sweep later drops it from the snapshot and marks its state expired.
Confirm both states while the window is open:
$ tokentriage keys list
KID STATE OWNER STUB EXPIRES LABEL
vk_9kQ2mX7tP4aZ active org:acme/team:ml ab12 2026-07-30T12:00:00Z ml-serving
vk_5rD8nB1uL0cQ active org:acme/team:ml 7f3c never ml-serving
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_9kQ2mX7tP4aZ --grace 0
rotated vk_9kQ2mX7tP4aZ -> vk_5rD8nB1uL0cQ (predecessor revoked immediately)
kid: vk_5rD8nB1uL0cQ
This secret is shown ONCE and cannot be recovered — copy it now:
ttv_…
Why this is honest and safe
Requests that ride the grace window are attributed to the successor kid, and the row records the predecessor on serving_key.rotated_from in the ledger, alongside a rotation_grace marker. That means an operator can always answer “whose key served this row, and what was it rotated from?” from the ledger itself — TokenTriage does not blur a rotation into a single ambiguous identity.
{
"decision": {
"governance": {
"rotation_grace": true,
"serving_key": {
"kid": "vk_5rD8nB1uL0cQ",
"node": "org:acme/team:ml",
"rotated_from": "vk_9kQ2mX7tP4aZ"
}
}
}
}
The lineage names public kids only — never secret material (keys live as SHA-256 hashes at rest, and the ttv_ prefix is stripped before any upstream forward, so a provider never sees a TokenTriage key). Every rotation is also written to the durable audit log with actor cli.