Skip to main content
The 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_stepup cookie (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

FieldTypeDefaultDescription
dryRunbooleanfalseWhen 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

StoreDB columnEncryption used
ConnectorsConnector.authConfigVersioned AES-256-GCM (encryption.ts)
Platform settingsPlatformSetting.valueBinary AES-256-GCM (platform-settings.ts)
SSO OIDC secretsSsoConfig.oidcClientSecretVersioned AES-256-GCM (encryption.ts)
MFA TOTP secretsUser.mfaSecretVersioned AES-256-GCM (encryption.ts)

Not covered (notRotated)

The response always includes a notRotated[] array for stores that cannot be rotated:
StoreReason
User.mfaBackupCodesSHA-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

{
  "dryRun": false,
  "keyVersion": "2",
  "connectors": { "total": 12, "rotated": 12, "errors": [] },
  "platformSettings": { "total": 4, "rotated": 4, "errors": [] },
  "sso": { "total": 1, "rotated": 1, "errors": [] },
  "mfa": { "total": 38, "rotated": 38, "errors": [] },
  "notRotated": [
    {
      "store": "User.mfaBackupCodes",
      "reason": "SHA-256 hashes — not encrypted; cannot be re-encrypted without the original plaintext codes"
    },
    {
      "store": "DSAR exports (dsar_exports.payload)",
      "reason": "stored as plaintext portable JSON (no ciphertext to rotate); encrypted-mode exports should be re-run with the current key; purged on the decisions retention clock"
    }
  ]
}
Each store returns { 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’s errors[] 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

When CONNECTOR_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

VariablePurpose
CONNECTOR_ENCRYPTION_KEYCurrent encryption key (required in production; omit for dev no-op mode)
CONNECTOR_ENCRYPTION_KEY_VERSIONVersion tag for the current key (default: "1")
CONNECTOR_ENCRYPTION_KEY_PREVIOUSPrevious key for decrypting old ciphertext during rotation
CONNECTOR_ENCRYPTION_KEY_PREVIOUS_VERSIONVersion tag for the previous key (default: "0")

Rotation workflow

  1. Generate a new key and set it as CONNECTOR_ENCRYPTION_KEY with CONNECTOR_ENCRYPTION_KEY_VERSION=2 (or your next version string).
  2. Set the old key as CONNECTOR_ENCRYPTION_KEY_PREVIOUS with CONNECTOR_ENCRYPTION_KEY_PREVIOUS_VERSION=1.
  3. Redeploy so the new env vars are live.
  4. Call POST /api/v1/encryption/rotate with { "dryRun": true } to verify what will be rotated.
  5. Call POST /api/v1/encryption/rotate (live pass). Check all stores report errors: [].
  6. For tenants with > 5,000 rows in any store, repeat step 5 until errors disappear.
  7. Once rotation is confirmed complete, you can safely clear CONNECTOR_ENCRYPTION_KEY_PREVIOUS.

Status codes

CodeCause
200Rotation (or dry-run) completed; inspect per-store errors[] for partial failures
400Invalid request body
401 / 403Missing tenant context, wrong role, or (session admins only) missing MFA step-up cookie
500Unexpected server error
  • 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