All Docs
FeaturesMaking Tax DigitalUpdated March 8, 2026

Encryption Key Rotation

Encryption Key Rotation

This platform stores highly sensitive data indefinitely — including HMRC OAuth tokens and National Insurance Numbers (NINOs). Periodic rotation of the application encryption key is a critical security practice and is required under HIPAA control HIPAA-06.

This page documents the tooling, schedule, and procedure for rotating encryption keys safely.


Overview

All sensitive PII fields are encrypted at rest using the key configured in your environment. Key rotation is the process of replacing that key with a new one while re-encrypting all existing ciphertext — ensuring that a compromised old key cannot be used to decrypt data going forward.

Three categories of data are in scope:

  • Bank account PII — account numbers, sort codes
  • HMRC OAuth tokens — access and refresh tokens
  • Org DB connection strings — per-organisation database credentials

Rotation CLI Tool

The script src/scripts/rotate-encryption-key.ts performs a full key rotation in a single database transaction.

Usage

npx ts-node src/scripts/rotate-encryption-key.ts \
  --old-key "<current-encryption-key>" \
  --new-key "<new-encryption-key>"

What the Script Does

  1. Reads every record containing an encrypted field from the database.
  2. Decrypts each field using the --old-key.
  3. Re-encrypts each field using the --new-key.
  4. Writes all updated records back inside a single atomic transaction — if any step fails, no changes are committed.
  5. Logs per-record progress to stdout and reports any individual failures without aborting the entire run.

Arguments

ArgumentRequiredDescription
--old-keyYesThe current encryption key (base64 or hex encoded)
--new-keyYesThe replacement encryption key

Safety Properties

  • Transactional — the database is never left in a partially-rotated state.
  • Non-destructive — the old key value is not written anywhere; it is only held in memory for the duration of the script.
  • Failure isolation — individual record failures are logged and counted but do not roll back the entire transaction unless they propagate to the transaction boundary.

Automated Rotation Reminder

An Inngest cron function monitors the date of the last successful key rotation (stored in the application config table) and alerts operators when the key has not been rotated in more than 90 days.

Alert Behaviour

  • The cron runs on a daily schedule.
  • If now - last_rotation_date > 90 days, an alert is dispatched to configured operator notification channels.
  • The last_rotation_date config entry must be updated after each successful rotation (see Post-Rotation Steps below).

Rotation Procedure

Follow these steps in order every time you rotate the encryption key.

1. Generate a New Key

Generate a cryptographically secure random key. Example using Node.js:

node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"

Store the new key securely (e.g. in your secrets manager) before proceeding.

2. Run the Rotation Script

npx ts-node src/scripts/rotate-encryption-key.ts \
  --old-key "$OLD_ENCRYPTION_KEY" \
  --new-key "$NEW_ENCRYPTION_KEY"

Review the log output. Confirm:

  • Total records processed matches the expected count.
  • Zero failures reported.

3. Update the Environment Variable

Replace the value of ENCRYPTION_KEY (or your configured key variable) with the new key in your deployment environment before the next deploy.

⚠️ Do not deploy the new key until the rotation script has completed successfully. Deploying early will cause decryption failures for any records not yet re-encrypted.

4. Deploy

Deploy the application with the updated ENCRYPTION_KEY. The application will now use the new key for all encryption and decryption operations.

5. Post-Rotation Steps

  • Update the last_rotation_date entry in the config table to today's date. This resets the 90-day Inngest alert timer.
  • Revoke / securely delete the old key from your secrets manager.
  • Record the rotation in your security audit log.

Rotation Schedule

TriggerAction
Every 90 days (recommended)Proactive scheduled rotation
Suspected key compromiseImmediate emergency rotation
Staff offboarding (key access)Rotation as part of offboarding checklist
Inngest 90-day alert firesRotate within 7 days

Background: Encrypted Fields

The following one-off migration scripts were used to encrypt data during initial setup. They are not used for ongoing rotation — use the CLI tool above instead.

ScriptScope
encrypt-bank-account-pii.tsBank account numbers and sort codes
encrypt-oauth-tokens.tsHMRC OAuth access and refresh tokens
encrypt-org-db-connection-strings.tsPer-org database connection strings

Compliance

This procedure satisfies HIPAA control HIPAA-06 (Encryption Key Management). A copy of this procedure is also maintained at docs/KEY-ROTATION.md in the repository.


Troubleshooting

Script exits with decryption errors

The most common cause is providing an incorrect --old-key. Verify the value matches the currently deployed ENCRYPTION_KEY exactly, including encoding (base64 vs hex).

last_rotation_date not found in config table

On first use, insert a row manually:

INSERT INTO config (key, value) VALUES ('last_rotation_date', NOW()::text);

Subsequent rotations update this row automatically via the post-rotation step.

Inngest alert keeps firing after rotation

Confirm the last_rotation_date config table entry was updated after the last rotation run. The Inngest function reads this value directly.