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.
- Envelope encryption. A fresh random 256-bit data-encryption key (DEK) per backup encrypts the artifact with AES-256-GCM. The DEK is wrapped (also AES-256-GCM) with a key-encryption key (KEK) returned by an
IBackupKeyProvider. - Opaque artifact. The ciphertext is a single blob prefixed with the Stella backup magic (
STLEBK01) — it is never recognisable as the underlying gzip/tar (filedoes not report “gzip compressed data”). - Signed manifest. A
BackupManifestcarries the ciphertext hash, plaintext hash, key id, algorithm, and the wrapped DEK. It is signed with HMAC-SHA256 using a separate signing key (key independence — TOPO-115 requirement). Restore verifies the manifest signature before decrypting. - Key independence. The encryption KEK and the manifest-signing key are distinct 32-byte keys returned by separate provider methods; the providers reject identical keys.
Key sources (recoverable outside the stack)
| Provider | Key source | Independent recovery |
|---|---|---|
StaticBackupKeyProvider | in-memory keys (e.g. STELLAOPS_BACKUP_ENCRYPTION_KEY / STELLAOPS_BACKUP_SIGNING_KEY env) | operator re-supplies the same secrets |
FileBackupKeyProvider | two key files on disk, stored outside the backed-up volumes | restore 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)
- Locate the encrypted artifact and its sidecar
<artifact>.manifest.json. - 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.
- 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.
- The decrypted artifact is the original gzip/tar evidence archive; its SHA-256 matches the
plaintextSha256recorded 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):
| Env | Meaning |
|---|---|
STELLAOPS_BACKUP_ENCRYPTION_KEY_FILE | path to the KEK file (preferred) |
STELLAOPS_BACKUP_SIGNING_KEY_FILE | path to the manifest-signing key file |
STELLAOPS_BACKUP_ENCRYPTION_KEY | KEK as base64/passphrase (written to a temp 0600 key file if no *_KEY_FILE set) |
STELLAOPS_BACKUP_SIGNING_KEY | signing key as base64/passphrase |
STELLAOPS_BACKUP_KEY_ID | key id recorded in the manifest |
STELLAOPS_BACKUP_CRYPTO_BIN | path 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.
Related documents
- Evidence Locker export format
- Evidence Locker architecture
- This supersedes the operator-DIY
pg_dump | gpg --encryptrecipe for evidence archive encryption.
