The app supports dual-secret JWT verification to allow zero-downtime secret rotation.
JWT_SECRET— the current signing secret (required)JWT_SECRET_PREVIOUS— the previous secret, accepted during the overlap window (optional)
- Token verification tries
JWT_SECRETfirst. - If verification fails and
JWT_SECRET_PREVIOUSis set, it retries with the previous secret. - Tokens verified via the previous secret are transparently re-issued using
JWT_SECRETand returned in theX-Refreshed-Tokenresponse header.
- Set
JWT_SECRET_PREVIOUSto the current value ofJWT_SECRET. - Generate a new secret and set it as
JWT_SECRET. - Deploy. Existing sessions continue to work via
JWT_SECRET_PREVIOUS. - After your session TTL has elapsed (all old tokens expired), remove
JWT_SECRET_PREVIOUS. - Deploy again to complete the rotation.
All wallet secret keys are encrypted with AES-256-GCM. Each encrypted value stores a keyVersion
so multiple key versions can coexist during a rotation window, enabling zero-downtime re-keying.
| Environment variable | Description |
|---|---|
WALLET_ENCRYPTION_KEY |
Hex-encoded 32-byte primary key (required) |
WALLET_ENCRYPTION_KEY_VERSION |
Integer version for the primary key (default: 1) |
WALLET_ENCRYPTION_KEY_V2 … _V10 |
Additional historic key versions kept for decryption |
Example — adding a new key as version 2 while keeping version 1 for backward compatibility:
WALLET_ENCRYPTION_KEY=<new-32-byte-hex>
WALLET_ENCRYPTION_KEY_VERSION=2
WALLET_ENCRYPTION_KEY_V1=<old-32-byte-hex>
- Generate a new 32-byte key:
openssl rand -hex 32 - Shift the current key to a versioned variable, e.g.
WALLET_ENCRYPTION_KEY_V1=<current-key>. - Set the new key:
WALLET_ENCRYPTION_KEY=<new-key>andWALLET_ENCRYPTION_KEY_VERSION=2. - Deploy — the service can now decrypt records encrypted with any loaded version.
- Re-encrypt all wallets via the admin endpoint:
The response reports
POST /api/v1/admin/system/rotate-encryption-key Authorization: Bearer <admin-jwt>{ total, rotated, skipped }. The operation is idempotent — safe to re-run if interrupted. - Verify — confirm
skipped === total(all records now use the latest key version). - Remove old key variables and redeploy to complete the rotation.
- Rotation progress is logged per-wallet at INFO level with
KeyRotationService. - The endpoint requires
JwtAuthGuard + AdminRoleGuard + IpAllowlistGuard. - Each
WalletBalanceEntityrow carries akeyVersioncolumn that tracks which key encrypted it.