Skip to content

JWT Signing-Key Rotation

Applies to: All subscriptions — today this applies to dev and QA only; production is not yet cut over to Key Vault, see Environment Variables

Purpose

Replace the secret that signs login sessions — on a schedule, or immediately after a suspected leak — without forcing every signed-in user to log in again.

Audience

The operator who runs a rotation, and the system administrator who confirms it.

Prerequisites

  • Key Vault Secrets Officer access on the target environment's vault
  • KEY_VAULT_URL set on every node of that environment

Steps

1. Know the one fact that matters

A rotation does not force a re-login as long as it stays inside the 48-hour overlap window. Outside that window — or on production, which is not yet on this path — changing the signing key ends every session immediately.

During the 48-hour window After the window
New logins get the new key Only the new key is accepted
A session from before the rotation still works A session signed with the retired key is rejected; that user must log in again
Both keys are logged as in use (see step 3) Only the new key should appear in the logs

2. Rotate the key

  1. In the target vault (complifly-kv-dev or complifly-kv-qa), copy the current value of jwt-signing-key into jwt-signing-key-previous.
  2. Generate a new value for jwt-signing-key. Record a new key identifier for it (for example 2026-10-b) — set JWT_KEY_ID to the new identifier and JWT_PREVIOUS_KEY_ID to the one just retired.
  3. Set JWT_PREVIOUS_VALID_UNTIL to the current time plus 48 hours.
  4. Restart the application on each node, one at a time, waiting for each to pass its health check before moving to the next.

3. Monitor the rotation

Every successful login check is counted and written to the audit trail as an aggregated event (not one row per request) — see Audit Log Review. Look for:

  • JWT_KEY_CURRENT_VERIFIED — logins succeeding on the new key.
  • JWT_KEY_PREVIOUS_VERIFIED — sessions still on the old key. This should fall to zero across the 48-hour window as old sessions expire or users re-authenticate naturally.
  • JWT_KEY_REJECTED — a token presented an unrecognised key identifier. A small, steady number is normal (stale browser tabs, old mobile sessions); a sudden spike after retiring the previous key means it was removed too early.

4. Retire the previous key

At or after JWT_PREVIOUS_VALID_UNTIL, confirm no sessions are still using the previous key (JWT_KEY_PREVIOUS_VERIFIED has fallen to zero), then remove jwt-signing-key-previous from the vault. It is not safe to remove it while that event is still being logged.

5. Emergency rotation (suspected key leak)

Run steps 2–4, but skip the overlap: set JWT_PREVIOUS_VALID_UNTIL to the current time (or leave jwt-signing-key-previous unset). Every session ends immediately and users sign in again once. This is a deliberate trade — a possibly compromised key staying valid for 48 more hours is the greater risk.

Validation

  • New logins succeed immediately after each node restarts.
  • Sessions that existed before the rotation keep working for the rest of the 48-hour window (confirm with one real account rather than assuming it).
  • The audit trail shows JWT_KEY_CURRENT_VERIFIED growing and JWT_KEY_PREVIOUS_VERIFIED falling toward zero across the window.
  • After retiring the previous key, no new JWT_KEY_PREVIOUS_VERIFIED events appear.

Troubleshooting

Symptom Cause Action
Users signed out shortly after a routine rotation JWT_PREVIOUS_VALID_UNTIL was set in the past, or omitted Confirm the value before restarting the next node
JWT_KEY_REJECTED spikes right after retiring the previous key Some sessions had not yet re-authenticated Expected for a small number; if it is large, the window was likely too short for this environment's traffic
Rotation on production forces every session out Production is not yet on Key Vault for JWT_SECRET Expected today — see Environment Variables for current status