All Docs
FeaturesagentOS Direct DebitUpdated March 13, 2026

Tenant Bank Detail Encryption at Rest

Tenant Bank Detail Encryption at Rest

From v1.0.11, the Direct Debit service encrypts tenant bank account numbers and sort codes at the application level before persisting them to the database. This page explains how the encryption works, what is and isn't encrypted, how keys are managed, and what you need to do when deploying.


Overview

Sensitive payment data — specifically tenant account numbers and sort codes — represents a high-value target. While database-level and disk-level encryption protect against storage compromise, application-level encryption provides an additional layer: even a full database dump yields only ciphertext that cannot be used without access to the application key.

This implementation satisfies:

  • PCI-DSS requirements for protecting stored sensitive authentication data.
  • FCA data minimisation obligations — only the minimum necessary data (masked digits) is ever exposed outside the encryption boundary.

Encryption Scheme

PropertyValue
AlgorithmAES-256-GCM
Key size256 bits
ModeAuthenticated Encryption with Associated Data (AEAD)
Key sourceKMS-managed key, injected via environment variable
IV/NonceRandomly generated per encryption operation

AES-256-GCM was chosen because it provides both confidentiality (ciphertext is unreadable without the key) and integrity (tampering with the ciphertext is detectable via the authentication tag). Each encryption operation uses a fresh random nonce, so identical plaintext values produce different ciphertext.


What Is Encrypted

FieldStored asNotes
account_numberAES-256-GCM ciphertextDecrypted only inside the Modulr submission job
sort_codeAES-256-GCM ciphertextDecrypted only inside the Modulr submission job
account_number_maskedPlaintext (last 4 digits)Safe for UI display and API responses

What is NOT encrypted:

  • The masked account number — this is intentionally stored in plaintext for display purposes.
  • Any other mandate or tenant fields (name, address, email, etc.) — these are not in scope for this encryption layer.

Key Management

The encryption key is managed externally via a KMS (Key Management Service) and delivered to the application as an environment variable:

BANK_DETAILS_ENCRYPTION_KEY=<base64-encoded 256-bit key>

Key management rules:

  • The key must never be committed to source control.
  • The key must never be written to application logs.
  • The key must never be stored in the database.
  • Key rotation requires a re-encryption migration (decrypt with old key, re-encrypt with new key) — do not swap the key without running the migration.
  • Access to the key in your KMS should be restricted to the application service identity and authorised operations staff only.

Decryption Boundary

Decryption is intentionally constrained to a single code path: the Modulr BACS submission job. This is the only point at which the full account number and sort code are needed — to construct the BACS payment instruction sent to Modulr.

┌────────────────────────────────────────┐
│  Application boundary                  │
│                                        │
│  Mandate form  ──► encrypt ──► DB      │
│                                        │
│  UI / API responses                    │
│    └─ account_number_masked only       │
│                                        │
│  Modulr BACS job                       │
│    └─ decrypt ──► submit to Modulr     │
│         └─ plaintext never leaves job  │
└────────────────────────────────────────┘

No API endpoint returns a decrypted account number or sort code. If a consuming application (such as agentOS) requests mandate details, it receives only the masked account number.


Masked Display

For display in UIs and API responses, a account_number_masked field is stored at mandate creation time. It contains only the last 4 digits of the account number, zero-padded where necessary:

Full account number:   12345678
Masked display value:  ****5678

This field is populated once at write time from the plaintext value and is never updated. The sort code is not displayed in any form in the UI.


API Behaviour

All mandate API responses that previously included account_number and sort_code now return:

{
  "account_number_masked": "****5678",
  "account_number": null,
  "sort_code": null
}

Or, depending on the API version, the encrypted fields may be omitted entirely from responses. Consuming applications should update any logic that relies on reading full account numbers or sort codes from the API — this data is no longer available outside the encryption boundary.


Deployment Guide

1. Provision the encryption key

Generate a cryptographically secure 256-bit key via your KMS and note the base64-encoded value. Store it as an environment variable:

export BANK_DETAILS_ENCRYPTION_KEY="<your-base64-256-bit-key>"

Ensure this variable is configured in your production environment before running the migration in step 2.

2. Run the encryption migration

A one-time migration script is provided to encrypt any existing plaintext bank detail records:

npm run migrate:encrypt-bank-details
# or
pnpm migrate:encrypt-bank-details

This script reads each mandate record with plaintext account_number / sort_code, encrypts them with the configured key, writes the ciphertext back, and populates account_number_masked. It is idempotent — already-encrypted records are skipped.

⚠️ Do not deploy v1.0.11 to production without running this migration first. Records written before the migration will fail to decrypt if the application attempts to process them.

3. Deploy the application

Deploy the new application version. From this point, all new mandate records will be encrypted at write time.

4. Verify

After deployment, confirm:

  • No plaintext account_number or sort_code values exist in the database (the migration script reports a summary).
  • A test BACS submission job completes successfully (confirming decryption works end-to-end).
  • API responses for mandate endpoints return only account_number_masked.

Environment Variables

VariableRequiredDescription
BANK_DETAILS_ENCRYPTION_KEY✅ YesBase64-encoded AES-256 key managed via KMS. Used to encrypt and decrypt tenant bank account numbers and sort codes.

Security Considerations

  • Key compromise — if the encryption key is exposed, all stored bank details are at risk. Rotate the key immediately using the re-encryption migration and revoke the old key in your KMS.
  • Backup encryption — ensure database backups are also subject to encryption at rest. The application-level encryption protects ciphertext in backups, but defence in depth is recommended.
  • Audit logging — the Modulr BACS submission job should be audit-logged so that every decryption event is traceable to a specific job run and collection record.
  • Nonce uniqueness — nonces are randomly generated per operation. With AES-256-GCM and a 96-bit nonce, the probability of collision is negligible at the volumes expected by this service.