# Rotate a runtime key

Replace the secret a running service sends, keeping its identity, with an overlap window so the rollout finishes before the old key stops.

Source: https://tokentriage.com/docs/build/rotate-a-runtime-key

---
import Callout from '../../../components/Callout.astro';
import Details from '../../../components/Details.astro';
import NextSteps from '../../../components/NextSteps.astro';

A runtime key sits in the environment of a service that is serving traffic. Something has happened
to it, or a quarter has passed, and it has to be replaced without a window where half your pods
hold a credential that has stopped working.

Rotation gives you that: new secret bytes for the same service identity, with both the old and the
new key accepted for a window you choose. Nothing downstream is re-pointed, because the identity a
deployment is bound to does not change. Rotation preserves the original expiry; it does not restart the lifetime. To renew an expiring credential, mint a new key for the same service identity and retire the old one after your rollout.

What you need before you start:

| You need | Where it comes from |
|---|---|
| `keys.manage` in the organization | the `workspace_admin` or `credential_manager` role, or an owner grant ([who can do what](/docs/build/organizations-and-access)) |
| the credential id (`vk_…`) | the document the mint printed, or the credentials list for its service identity. The key's own value is not usable here and no flag accepts one |
| a management key on `TOKENTRIAGE_MANAGEMENT_KEY` | `cloud key` in the same organization |
| the deployment's origin | `--base-url` has no default, so a management CLI cannot address the wrong estate by guessing |

You do **not** need the current key's value, a maintenance window, or a new service identity.

## 1. Mint the successor

One command replaces the secret. `--wait` then polls until the new credential's data-path grant has
been acknowledged, and reports that on stderr, so the JSON document on stdout stays a document.

```console
$ tokentriage cloud credential rotate \
    --base-url https://app.example.com --org org_ACME \
    --credential-id vk_ABCDEFGHJKLM --wait
```

The answer is written to stdout as JSON before anything else is attempted, and it is the only place
the new secret ever appears: `credential` carries the metadata (`kid`, `service_id`, `environment_id`,
`gateways`, `readiness`, `operation_id`), and `secret` carries the value once. The server keeps a
digest of it. If you lose it, nothing can show it again, and the recovery is to rotate again.

<Callout type="warning">

A successful rotation reveals the successor secret once. If you lose that response, the secret
cannot be recovered. Read the service identity’s credential list to identify the successor, then rotate that credential to mint a fresh secret. Revoke the credential you cannot use — leaving it to
lapse leaves an accepted key nobody holds. If the request timed out or failed, inspect the operation and credential list before starting a new rotation; do not assume the first attempt succeeded.

</Callout>

## 2. Roll the new secret out inside the window

Both credentials work until the overlap closes. The default window is **15 minutes**; the flag
accepts 0 to 1440 minutes, and the lane's own ceiling is 24 hours.

| What you pass | What happens to the predecessor |
|---|---|
| nothing | accepted for 15 more minutes, then it lapses |
| `--overlap-minutes N` | accepted for N more minutes |
| `--overlap-minutes 0` | closed as the rotation completes |
| `--revoke-previous-now` | its grant is withdrawn during this call |

`--overlap-minutes` and `--revoke-previous-now` are mutually exclusive, because "immediately" and
"for N minutes" cannot both be the answer.

Use the window to deploy the new value the way you deploy any secret, and let the replicas turn
over. Both keys resolve to the same service identity, the same environment and the same gateway
allowlist, so a request is metered once whichever one it presented. A request that presents the
successor while the predecessor is still accepted is disclosed on its decision row as
`rotation_grace`, which is how you can tell a fleet has finished turning over.

**Keep the overlap if you might roll back.** A rollback does not resurrect a revoked predecessor.
Rolling a deployment back to an image holding the old value works only while that value is still
accepted.

## 3. Close the window when the rollout is done

Waiting for the window to lapse is a valid ending. To close it earlier, revoke the predecessor by
id:

```console
$ tokentriage cloud credential revoke \
    --base-url https://app.example.com --org org_ACME \
    --credential-id vk_ABCDEFGHJKLM
```

The answer is a typed acknowledgement reporting two separate facts: `grant_withdrawn`, whether the
data-path grant was taken away, and `revoked_at`, when the credential was marked. They are separate
because the first is the one that stops traffic.

New dispatch stops within a published bound of 30 seconds, which is push invalidation plus a
bounded authority lease and its refresh, with clock skew counted in. It is not zero. Requests
already sent to a provider finish and may cost money; revocation closes the door and does not reach
through it. A second revocation of the same credential answers `409` rather than a quiet success.

## What "ready" means, and what it doesn't

A minted credential and a working credential are two states, and the word that separates them is
published rather than guessed at.

| Readiness | Accepts requests? | How you enter it | How you leave it |
|---|---|---|---|
| `pending` | no | a mint or rotation is recorded, and the data-path grant is not acknowledged yet | the grant is acknowledged, or the operation fails |
| `ready` | yes | the grant was acknowledged | revocation, or expiry |

There is no third word: "probably ready" is not an operational answer. `--wait` polls for up to 30
seconds by default, with a ceiling of 5 minutes, and `cloud credential status --operation <ID>`
reads the same fact later. A wait that times out has not failed the rotation; it has stopped
watching.

Revocation is terminal. This lane publishes no reversible disabled state between accepted and
revoked, so a credential you revoke is replaced rather than re-enabled.

<Details summary="What a rotation cannot widen">

The successor's environment, its gateway allowlist and its actions are read from the predecessor's
own record rather than re-supplied on the rotate call. So a rotation cannot quietly add a gateway,
and a credential that was admitted at one gateway in Development stays that after any number of
rotations. Widening is a new mint, which names `--env` and `--gateway` and is a decision somebody
made.

Two more checks run before a successor is minted: the application must still be active, and the
environment must still be enabled. A rotation into a retired environment would carry a live
credential past the decision to retire it, so it is turned away and `revoke` is the honest verb
there.

</Details>

## What can go wrong

**A 401 after the rotation, on some replicas only.** Those replicas still hold the predecessor and
the window has closed. Rotate again and give the rollout a longer `--overlap-minutes`.

**A 403 naming a gateway or an environment.** That is scope, not the key: a runtime credential is
admitted at the gateways and the one environment its record names, and the rotation changed neither.
Read [connect your app](/docs/get-started/connect-your-app) for which selector headers a request
carries.

**Readiness stuck on `pending`.** The mint is durable and the grant has not been acknowledged.
Re-read it with `cloud credential status`; a request with the new key may still be turned away until
the word is `ready`.

## What this page doesn't cover

- Replacing the *provider* key behind a gateway, which is a different credential with a different
  lifecycle: [Add a saved provider connection](/docs/build/provider-connections#4-replace-the-key).
- Rotating a virtual key in a self-hosted deployment, which is a separate mechanism: [Rotate a
  virtual key with zero downtime](/docs/recipes/rotate-a-virtual-key).
- Every flag on the four credential verbs: [Hosted runtime
  credentials](/docs/reference/cli#hosted-runtime-credentials).

**Availability.** These operations are part of this private test release. The 30-second bound is
the published design bound; its end-to-end measurement across workers and restarts is part of the
release's own acceptance run.

<NextSteps items={[
  { title: 'Connect your app', href: '/docs/get-started/connect-your-app', description: 'Where the runtime key goes, and the two headers beside it.' },
  { title: 'Who can do what in your company', href: '/docs/build/organizations-and-access', description: 'The grant that lets somebody hold and replace keys, and what it does not include.' },
  { title: 'Add a saved provider connection', href: '/docs/build/provider-connections', description: 'The provider credential behind the gateway, and how its replacement differs.' }
]} />
