Skip to content

Encryption at rest

Nimbus encrypts the local database files it owns: embedded SQLite and redb tenant databases, the redb control-plane database, and libSQL replica caches. Encryption is disabled by default and is enabled with the --encryption-key-provider flag on nimbus start. External databases (Postgres, MySQL, a remote libSQL primary) are encrypted by that database, not by Nimbus.

For the full flag ↔ environment variable ↔ config-key table, see the configuration reference.

  1. Generate a 32-byte key file and lock down its permissions. Store it outside the data directory:

    Terminal window
    openssl rand -out /secure/path/master.key 32
    chmod 400 /secure/path/master.key
  2. Start Nimbus with encryption enabled:

    Terminal window
    nimbus start \
    --encryption-key-provider master-key-file \
    --encryption-master-key-file /secure/path/master.key

New tenant databases are created encrypted from then on. Nimbus generates a random data-encryption key (DEK) per protected file and stores the wrapped DEK in a sidecar manifest at <file>.nimbus-enc. Keep manifests with their database files — a database without its manifest cannot be opened.

If you enable encryption on a server that already has plaintext database files, startup fails for those files with a “sidecar manifest is missing” error. Migrate them first — see Migrate existing plaintext data.

Local fileCipher
Embedded SQLite tenant databasesSQLCipher
Embedded redb tenant databasesAES-256-GCM-SIV (per page)
Control-plane redb database (nimbus-control.db)AES-256-GCM-SIV (per page)
libSQL replica cache filesSQLCipher

With --tenant-provider postgres or mysql, only the local control-plane database is encryptable by Nimbus; tenant data encryption belongs to the external database.

Exactly one provider is active at a time. Flags belonging to a provider you have not selected are rejected at startup, and encryption flags without --encryption-key-provider are also rejected.

One operator-managed 32-byte key file. Nimbus derives a unique wrapping key per protected file from it with HKDF-SHA256, so the single key protects many databases without reusing DEKs:

Terminal window
nimbus start \
--encryption-key-provider master-key-file \
--encryption-master-key-file /secure/path/master.key

The file must contain exactly 32 bytes of raw key material — not hex, not base64.

One 32-byte wrapping-key file per protected subject, for deployments that want explicit per-tenant or per-role key custody:

Terminal window
nimbus start \
--encryption-key-provider key-dir \
--encryption-key-dir /secure/path/keys/

Key files are named by hex-encoding the subject descriptor recorded in the manifest, with a .key extension. For example, the descriptor db:sqlite:tenant:demo:demo.sqlite3 maps to 64623a73716c6974653a74656e616e743a64656d6f3a64656d6f2e73716c69746533.key. Each file must contain exactly 32 raw bytes.

Envelope encryption with AWS KMS-managed wrapping keys. The per-file manifest contract is unchanged — KMS replaces only the wrapping step:

Terminal window
nimbus start \
--encryption-key-provider aws-kms \
--encryption-aws-kms-key-id alias/nimbus-production \
--encryption-aws-region us-east-1

--encryption-aws-kms-key-id is required; region and --encryption-aws-endpoint-url (for VPC endpoints or local KMS emulators) are optional, falling back to the standard AWS credential and region chain. Nimbus calls GenerateDataKey to create DEKs, Decrypt to reopen databases, and ReEncrypt during key rotation, and binds manifest metadata into the KMS EncryptionContext — so grant the server identity those KMS permissions on the selected key. A tampered manifest or wrong key surfaces as a decrypt failure, not silent corruption.

The nimbus encryption admin commands (status, migrate, export, rotate-kek, rotate-dek) do not take server flags. They read the same persistence and encryption settings from environment variables (NIMBUS_ENCRYPTION_*, NIMBUS_DATA_DIR, …) or from the JSON config file named by NIMBUS_CONFIG. Set those before running any admin command:

Terminal window
export NIMBUS_ENCRYPTION_KEY_PROVIDER=master-key-file
export NIMBUS_ENCRYPTION_MASTER_KEY_FILE=/secure/path/master.key

Every command below assumes this environment is in place. The commands refuse to run when encryption is not configured.

From the CLI:

Terminal window
nimbus encryption status # human-readable
nimbus encryption status --format json

This reports the enabled state, the key provider, and which local file families are covered under the current provider configuration.

On a running server, the same posture is available over HTTP. The endpoint is admin-only — send the local admin token (see the self-host quickstart for where the token lives):

Terminal window
curl -s http://localhost:8080/debug/encryption/status \
-H "Authorization: Bearer $NIMBUS_TOKEN"
{
"enabled": true,
"encrypted_families": ["embedded_sqlite", "control_plane_redb"],
"descriptor": {
"status": "enabled",
"provider": "master_key_file",
"path": "/secure/path/master.key"
}
}

The response never contains key material.

Stop the server before migrating files it has open. Then convert each plaintext database into an encrypted copy:

Terminal window
nimbus encryption migrate \
--source /data/tenant-a.sqlite3 \
--provider sqlite \
--tenant-id tenant-a
  • --provider is sqlite, redb, or libsql-cache.
  • --target is optional; the default appends .encrypted to the source name. The target (and its manifest) must not already exist.
  • --tenant-id names the owning tenant. For redb it may be omitted only for nimbus-control.db.
  • Migration validates the encrypted copy afterwards unless you pass --skip-validation, and publishes the target only after the copy succeeds.
  • Pass --retire-source to delete the plaintext source (and its SQLite sidecar files) after a successful migration.

libSQL replica caches are not migrated this way: restart the server with encryption enabled and the cache rebuilds from the remote primary under the new key.

KEK rotation rewraps the manifest only — database pages are not rewritten. Run it with the current provider configured in the environment, and name the replacement provider on the command:

Terminal window
NIMBUS_ENCRYPTION_KEY_PROVIDER=master-key-file \
NIMBUS_ENCRYPTION_MASTER_KEY_FILE=/secure/path/old.key \
nimbus encryption rotate-kek \
--path /data/tenant-a.sqlite3 \
--new-master-key-file /secure/path/new.key
  • --path is the protected database file; pass --all with a directory path to rotate every .nimbus-enc manifest in that directory.

  • The replacement provider is inferred when you pass exactly one of --new-master-key-file, --new-key-dir, or --new-aws-kms-key-id; set --new-key-provider explicitly otherwise.

  • To rotate onto AWS KMS:

    Terminal window
    nimbus encryption rotate-kek \
    --path /data/tenant-a.sqlite3 \
    --new-key-provider aws-kms \
    --new-aws-kms-key-id alias/nimbus-production \
    --new-aws-region us-east-1

After rotation, use the new key configuration for all subsequent starts and admin commands.

DEK rotation replaces the per-file key and is provider-specific:

Terminal window
nimbus encryption rotate-dek \
--path /data/tenant-a.sqlite3 \
--provider sqlite \
--tenant-id tenant-a
  • sqlite — checkpoints WAL state, backs up the database artifact set to .bak copies, rekeys in place with SQLCipher’s PRAGMA rekey, then updates the manifest. On failure the backups are restored automatically.
  • redb — re-encrypts every page into a staged file under fresh nonces, then commits the staged database and manifest together. An interrupted rotation is recovered automatically on the next rotate or server start.
  • libsql-cache — rotates the manifest to a fresh DEK and deletes the local cache files; stop any running instance first, and the next start rebuilds the cache from the remote primary under the new key.

Pass --skip-backup to skip the .bak copies if you have your own backup in place.

To export an encrypted database back to plaintext (disaster recovery, or moving data somewhere that needs plaintext):

Terminal window
nimbus encryption export \
--source /data/tenant-a.sqlite3 \
--target /recovery/tenant-a.sqlite3 \
--provider sqlite \
--tenant-id tenant-a

--target is required and must not exist. libSQL caches cannot be exported — rebuild them from the remote primary instead.

Backup rules:

  • Back up the .nimbus-enc manifest sidecars together with the database files. KMS access alone cannot recover a database whose manifest is lost.
  • Back up the key material itself — the master key file or key directory — or, for aws-kms, preserve access to the KMS key.
  • Encrypted backups without the keys are unrecoverable. Test restores before you depend on them. See backup and restore.
  • Key file missing or unreadable — the configured master key or per-subject key file path does not exist or is not readable. Startup fails before serving traffic.
  • Wrong key size — key files must be exactly 32 bytes of raw binary data, not hex or base64.
  • “sidecar manifest is missing” — encryption is enabled for an existing plaintext file. Migrate it first (see above).
  • Cannot open an encrypted database — the configured provider cannot unwrap the DEK in the manifest, or the file and manifest no longer match. Verify the key material and restore the file/manifest pair from backup.
  • AWS KMS errors — key-not-found means the key ID or alias does not resolve in the selected region; permission-denied means the caller lacks the KMS operation on the key; network errors point at region, endpoint override, VPC routing, or the AWS credential chain.