Encrypted Backup + Independent Key Recovery

Status: implemented for SPRINT_20260520_088 (closes TOPO-115).

Purpose

Evidence backups must be encryptable as a product feature rather than an operator-DIY pg_dump | gpg recipe. This document describes the EvidenceLocker backup-encryption layer: an offline / air-gap-capable software KMS that turns an exported evidence archive into an opaque, authenticated artifact accompanied by a signed backup manifest, with an encryption key that is recoverable independently of the running stack.

Design

The encryptor is a KMS-style abstraction (IKmsBackupEncryptor) with an offline software default (LocalKmsBackupEncryptor). No external KMS dependency is vendored — all primitives come from System.Security.Cryptography.

Key sources (recoverable outside the stack)

ProviderKey sourceIndependent recovery
StaticBackupKeyProviderin-memory keys (e.g. STELLAOPS_BACKUP_ENCRYPTION_KEY / STELLAOPS_BACKUP_SIGNING_KEY env)operator re-supplies the same secrets
FileBackupKeyProvidertwo key files on disk, stored outside the backed-up volumesrestore operator reads the same files; no service/DB needed

Each key value may be a raw 32-byte base64 key, or a passphrase that is hashed to 32 bytes with SHA-256 (deterministic, offline).

Restore procedure (independent key recovery)

  1. Locate the encrypted artifact and its sidecar <artifact>.manifest.json.
  2. Recover the KEK and signing key from outside the running stack (the key files / escrowed secrets). The EvidenceLocker service does not need to be running.
  3. Construct the same provider from the recovered keys and call IKmsBackupEncryptor.DecryptAsync(ciphertext, manifest, plaintext). This:
    • verifies the manifest HMAC signature (fails closed on tamper / wrong key),
    • verifies the ciphertext SHA-256 matches the manifest,
    • unwraps the DEK with the recovered KEK,
    • decrypts and verifies the round-trip plaintext SHA-256 matches the manifest.
  4. The decrypted artifact is the original gzip/tar evidence archive; its SHA-256 matches the plaintextSha256 recorded at backup time (lossless).

Wiring

AddLocalBackupEncryption(IBackupKeyProvider) registers the software encryptor. Exports requested with ExportConfiguration.EncryptArchive = true then emit the encrypted artifact + signed manifest. If encryption is requested but no encryptor is registered, the exporter fails closed (KEYS_NOT_AVAILABLE) — it never silently emits plaintext.

Compose backup.sh wiring (operator path)

devops/compose/scripts/backup.sh has an optional, default-off encrypt step gated by STELLAOPS_BACKUP_ENCRYPT=1. When enabled it runs the plaintext stellaops-backup-*.tar.gz through the product encryptor and removes the plaintext, leaving only the opaque *.stlebk artifact + *.stlebk.manifest.json. Unset (the default) keeps the prior plaintext-tar behaviour — backward compatible.

Key sources (recoverable independently of the running stack; store outside the backed-up volumes):

EnvMeaning
STELLAOPS_BACKUP_ENCRYPTION_KEY_FILEpath to the KEK file (preferred)
STELLAOPS_BACKUP_SIGNING_KEY_FILEpath to the manifest-signing key file
STELLAOPS_BACKUP_ENCRYPTION_KEYKEK as base64/passphrase (written to a temp 0600 key file if no *_KEY_FILE set)
STELLAOPS_BACKUP_SIGNING_KEYsigning key as base64/passphrase
STELLAOPS_BACKUP_KEY_IDkey id recorded in the manifest
STELLAOPS_BACKUP_CRYPTO_BINpath to a published stellaops-backup-crypto binary (air-gap); otherwise the script uses the published CLI under src/EvidenceLocker/__Apps/.../publish or builds it from source

backup.sh shells out to stellaops-backup-crypto(src/EvidenceLocker/__Apps/StellaOps.EvidenceLocker.BackupCli), a thin CLI over LocalKmsBackupEncryptor — the same audited crypto path the service uses (no shell crypto reimplementation). Restore is the inverse:

stellaops-backup-crypto decrypt \
  --in  stellaops-backup-<ts>.tar.gz.stlebk \
  --out stellaops-backup-<ts>.tar.gz \
  --manifest stellaops-backup-<ts>.tar.gz.stlebk.manifest.json \
  --kek-file <kek.key> --signing-key-file <sign.key>
# then extract the decrypted tar.gz into the target volumes as usual.