POST /api/v1/encryption/rotate route re-encrypts all ciphertext held for the calling tenant under the current key version. It covers every store that uses the platform’s AES-256-GCM versioned encryption, and runs as a dry-run or a live pass depending on the dryRun flag.
POST /api/v1/encryption/rotate
Auth: admin role + tenant scope. MFA posture depends on the caller:- Session admins — the middleware step-up gate requires a valid, fresh
kaireon_stepupcookie (the same 15-minute HMAC proof that protects all admin writes) before this write is allowed. See MFA enforcement. krn_API-key callers — these carry no session JWT, so the middleware MFA gate is skipped. This is the platform-wide posture for all admin writes via API key, not specific to this route — key possession alone gates them.
Request body
| Field | Type | Default | Description |
|---|---|---|---|
dryRun | boolean | false | When true, performs a decrypt-only pass and counts what would rotate — no writes are made. Dry runs are audit-logged so they appear in compliance evidence. |
Covered stores
| Store | DB column | Encryption used |
|---|---|---|
| Connectors | Connector.authConfig | Versioned AES-256-GCM (encryption.ts) |
| Platform settings | PlatformSetting.value | Binary AES-256-GCM (platform-settings.ts) |
| SSO OIDC secrets | SsoConfig.oidcClientSecret | Versioned AES-256-GCM (encryption.ts) |
| MFA TOTP secrets | User.mfaSecret | Versioned AES-256-GCM (encryption.ts) |
Not covered (notRotated)
The response always includes a notRotated[] array for stores that cannot be rotated:
| Store | Reason |
|---|---|
User.mfaBackupCodes | SHA-256 hashes — not encrypted; the original plaintext codes are not stored so they cannot be re-encrypted |
DSAR exports (dsar_exports.payload) | Stored as plaintext portable JSON — there is no ciphertext to rotate. Encrypted-mode exports should be re-run with the current key. Payloads are purged on the decisions retention clock. |
Response
{ total, rotated, errors[] }. total is the number of rows fetched; rotated is the count successfully re-encrypted (or that would be, in dry-run); errors[] holds per-row error strings.
Row cap
Each store is capped at 5,000 rows per pass. When a store exceeds the cap, the pass is truncated and the store’serrors[] array contains a message indicating the cap was hit. Re-running the route does not auto-advance past the already-rotated rows — the cap applies to the first 5,000 rows fetched by the WHERE clause on every run. Tenants with more than 5,000 rows in a store need staged rotation (e.g. run the route repeatedly until all errors disappear).
Keyless development
WhenCONNECTOR_ENCRYPTION_KEY is not set (local development), the platform-settings rotation performs a clean no-op. All other stores behave normally because they derive their key from the same env var.
Environment variables
| Variable | Purpose |
|---|---|
CONNECTOR_ENCRYPTION_KEY | Current encryption key (required in production; omit for dev no-op mode) |
CONNECTOR_ENCRYPTION_KEY_VERSION | Version tag for the current key (default: "1") |
CONNECTOR_ENCRYPTION_KEY_PREVIOUS | Previous key for decrypting old ciphertext during rotation |
CONNECTOR_ENCRYPTION_KEY_PREVIOUS_VERSION | Version tag for the previous key (default: "0") |
Rotation workflow
- Generate a new key and set it as
CONNECTOR_ENCRYPTION_KEYwithCONNECTOR_ENCRYPTION_KEY_VERSION=2(or your next version string). - Set the old key as
CONNECTOR_ENCRYPTION_KEY_PREVIOUSwithCONNECTOR_ENCRYPTION_KEY_PREVIOUS_VERSION=1. - Redeploy so the new env vars are live.
- Call
POST /api/v1/encryption/rotatewith{ "dryRun": true }to verify what will be rotated. - Call
POST /api/v1/encryption/rotate(live pass). Check all stores reporterrors: []. - For tenants with > 5,000 rows in any store, repeat step 5 until errors disappear.
- Once rotation is confirmed complete, you can safely clear
CONNECTOR_ENCRYPTION_KEY_PREVIOUS.
Status codes
| Code | Cause |
|---|---|
200 | Rotation (or dry-run) completed; inspect per-store errors[] for partial failures |
400 | Invalid request body |
401 / 403 | Missing tenant context, wrong role, or (session admins only) missing MFA step-up cookie |
500 | Unexpected server error |
Related
- MFA enforcement — the step-up gate protecting session-admin writes to this route
- DSAR — export payloads that age out independently of this rotation
- Security hardening — operator checklist