Encryption at Rest (opt-in envelope encryption)

This guide covers NovaFabric's optional application-layer envelope encryption for capsule payloads in the object capsule store (ADR-0185).

Status: experimental (v0.61, shipped 2026-07-16) — and never the default. Out of the box, NovaFabric's at-rest posture is integrity, not confidentiality: DSSE envelopes, Merkle logs, and WORM Object Lock make evidence tamper-evident, while confidentiality is delegated to disk, database, and bucket encryption. That remains the recommended default. Turn this feature on only when you need application-held confidentiality on top — and understand the trade first:

Lose the KEK, lose the evidence. Envelope encryption trades confidentiality for a key-availability risk. A destroyed or unreachable key makes every encrypted capsule permanently unreadable — inside its WORM retention window, with no recovery path. Fold the KEK into your backup and DR posture (see the backup & restore runbook) before enabling this in anything you care about.


1. How it works

Per-object envelope scheme (src/novafabric/trust/envelope_encryption.py):

  1. Every put generates a fresh random 256-bit DEK (data-encryption key) and encrypts the payload with AES-256-GCM (96-bit random nonce), using the existing cryptography dependency — no new dependency. Two encryptions of identical plaintext always produce distinct ciphertexts and distinct wrapped DEKs.
  2. The DEK is wrapped by a KEK (key-encryption key) held by the operator's key backend, via the additive KeyWrappingBackend capability (wrap_key / unwrap_key / kek_ref). The wrapped DEK travels inside the stored envelope; the KEK never does.
  3. Encrypt-before-WORM (normative ordering). The serialized ciphertext envelope is what the WORM adapter stores; the SHA-256 handed to the backend (checksum header / CAS gate) is recomputed over the stored encrypted bytes. content_sha256 inside the envelope is likewise computed over the ciphertext — so integrity verification (hashes, Merkle leaves, WORM conformance) never requires decryption or KMS access.
  4. Reads are transparent. The store detects the envelope by its schema marker fields and decrypts; objects written before encryption was enabled pass through unchanged, so mixed stores keep working.
  5. Chain-log objects are never encrypted. They are integrity metadata, not capsule payloads — exactly as ADR-0031 excludes them from WORM.

Tampering is loud, with named exceptions: a ciphertext that fails its recorded hash raises CiphertextIntegrityError before any key material is touched; a tampered wrapped key raises DekUnwrapError; AES-GCM authentication failure raises BlobAuthenticationError.

2. Enabling it (store wiring)

Opt-in requires both environment variables on the process that owns the object capsule store:

# 256-bit KEK: raw 32 bytes, or 64 hex characters, in a local file
head -c 32 /dev/urandom > /secure/nova-kek.bin
chmod 600 /secure/nova-kek.bin

export NOVA_OBJECT_STORE_ENCRYPTION=1        # truthy: 1/true/yes/on
export NOVA_OBJECT_STORE_KEK_PATH=/secure/nova-kek.bin

With both set, make_adapter wraps whichever WORM backend you configured (s3, minio, ceph_rgw, azure_blob, local) in the encrypting adapter, and every capsule payload is envelope-encrypted before the WORM write. Absent that configuration, behavior is byte-for-byte unchanged.

The local-file KEK path uses the NovaSeal LocalSigningBackend wrap capability — suitable for dev/test parity and self-managed deployments where you control the file's lifecycle.

Per-tenant KEKs (ADR-0243, experimental)

Optionally, each tenant's capsules can wrap their DEKs under that tenant's own KEK instead of the shared one — key compromise becomes tenant-scoped, and removing one tenant's KEK makes exactly that tenant's data cryptographically unreadable (the offboarding/erasure semantic, enforced fail-closed with an error naming the tenant):

mkdir /secure/tenant-keks
head -c 32 /dev/urandom > /secure/tenant-keks/acme.kek     # one file per tenant
export NOVA_OBJECT_STORE_TENANT_KEK_DIR=/secure/tenant-keks

Tenants without a <tenant>.kek file keep using the default KEK — zero change for existing deployments — and every pre-existing envelope stays readable forever (the tenant_key_id field is additive). The registry resolves tenants from the capsules/{tenant}/… key layout the store already uses. Customer-held cloud KMS keys (BYOK), rotation campaigns, and the maker-checker tenant-keys shred command are later ADR-0243 slices — planned, not implemented.

3. Crypto-shred

Deleting the one wrapped DEK erases an object cryptographically while the ciphertext stays untouched inside its WORM retention window — and still verifies via content_sha256. This is the single-key-deletion semantic that composes with the retention scheduler (ADR-0134): "deletion is evidence" holds even for content that WORM will not let you physically remove.

A shredded envelope is permanently unrecoverable; reads raise the named ShreddedBlobError rather than returning garbage. Shredding is idempotent. Today shred() is a Python API (novafabric.trust.envelope_encryption.shred); there is no CLI command for it yet.

4. Honest limits — read before production

5. Operational checklist


See also