TILLAUTH · OPERATE

Rotate per app, atomically.

Each app has a data encryption key. Rotating it re-encrypts every sensitive row in one transaction — no half-rotated state, no orphan plaintext, no "we ran a migration and lost a TOTP secret" stories.

Two layers

  • DEK · Data Encryption Key — per app. AES-256, used to GCM-encrypt sensitive blobs: OAuth refresh tokens, OAuth client secrets, TOTP secrets, webhook signing secrets. Each row carries its own nonce.
  • KEK · Key Encryption Key — per org, the org's key-encryption key (KEK). Wraps the DEK with AES-256-GCM (key-wrap mode). The wrapped DEK is what's persisted alongside the app record.

Compromising either layer alone is not sufficient: stealing the DB gets wrapped DEKs but no KEK; stealing the KEK gets you nothing without the DB. Rotation is about minimising the window during which a stolen DEK is useful.

What rotation does

Triggered from the per-app Settings tab (or the admin API). We:

  1. Begin a transaction.
  2. Generate a new 32-byte DEK. KEK-wrap it.
  3. For all per-app encrypted fields (OAuth client secrets, TOTP seeds, tokens, webhook signing secrets): decrypt with old DEK, re-encrypt with new DEK (new nonce per row), update the row.
  4. Update the persisted wrapped DEK to the new value.
  5. Commit.
  6. Audit row: dek.rotated with metadata.row_count.

If anything fails in the middle, the transaction rolls back — you're still on the old DEK. No partial state is possible.

When to rotate

  • Suspected DB compromise. Rotate immediately, then rotate KEK too.
  • Employee offboarding. If the person who left had access to backups, rotate.
  • Regular cadence. Quarterly is more than enough; we don't recommend tighter.
  • Migrating an app between orgs — currently a manual operation; rotation happens as part of it.

Performance

The transaction holds row locks on the four tables for the duration of the re-encrypt — typically sub-second for <1000 users, a few seconds for tens of thousands. While the rotation is in flight, signin and admin endpoints that touch those rows briefly wait; runtime endpoints (JWT verification) are unaffected since they don't read the encrypted columns.

For very large apps (millions of users with stored OAuth refresh tokens), schedule rotation during a quiet window. The dashboard shows the row count before you confirm.

KEK rotation

Rotating the KEK is an infrastructure operation, not a per-app one. Replace TILLAUTH_KEK with both old and new during a deploy window; the backend re-wraps each app's DEK lazily on its next access and writes back the updated wrapped DEK. After every app has been touched (or after 90 days), the old KEK can be retired.

KEK rotation does not re-encrypt the underlying blobs — those stay under their existing per-app DEKs. Only the wrappers change.

Failure modes

  • Decrypt fails mid-rotation → transaction rolls back. Most likely cause: a row was encrypted with a different DEK (corruption). Audit row: dek.rotated with result='error' and the decrypt error in reason.
  • KEK missing → the new wrapped DEK can't be produced; rotation refuses to start. Returns 500 with a clear error.
  • Rate limit → rate-limited to prevent thrashing. Stops accidental thrash from scripts.

Next: custom domains for branded login URLs · orthe Node SDK for the verifier side.