
Encryption
Encryption
Server-side envelope encryption with chunked AES-256-GCM. When enabled, objects are encrypted before being stored on backends and decrypted transparently on read. Exactly one key source is required.
Generating a master key:
Key source options — exactly one of the following must be set:
| Source | Config field | When to use |
|---|---|---|
| Inline | master_key | Base64-encoded 256-bit key in config or env var. Simplest option. |
| File | master_key_file | Path to a file containing exactly 32 raw bytes. Good for bare-metal with config management. |
| Vault Transit | vault | Delegate key wrapping/unwrapping to HashiCorp Vault. Best for production with HSM-backed key management. |
Vault Transit configuration:
The Vault Transit engine handles wrapping and unwrapping DEKs — the orchestrator never sees the master key material. The key_name must reference an existing key in the Transit engine.
Key rotation support:
When rotating to a new master key, move the old key to previous_keys so existing objects can still be decrypted:
After updating the config, call the rotate-encryption-key admin API to re-wrap all DEKs with the new key. See Rotating encryption keys below.
Important notes:
- Encryption is not reloadable — changing encryption settings requires a restart.
- The
chunk_sizemust stay the same for the lifetime of the data. Changing it after objects are encrypted will make those objects unreadable. - Encrypted objects are slightly larger than their plaintext (header + per-chunk overhead). The exact overhead is: 32 bytes (header) + 28 bytes per chunk (nonce + auth tag).
Integrity verification
SHA-256 content hashing for data integrity verification. When enabled, objects are checksummed on write and the hash is stored alongside the object location in PostgreSQL.
How it works:
- Write path: SHA-256 is computed on the plaintext body (before encryption) and stored in
object_locations.content_hash. - Read path (
verify_on_read): AVerifyingReaderwraps the response body and computes the hash as data streams to the client. On mismatch at EOF, the corrupted copy is enqueued for cleanup. - Scrubber: A background worker periodically reads random objects from backends, decrypts if needed, and verifies their hash. Corrupted copies are enqueued for cleanup. Each read counts against the backend’s usage quota.
- Backfill: Objects written before integrity was enabled have no stored hash. Use
admin backfill-checksumsto read those objects and compute their hashes.
Integrity is hot-reloadable — changes take effect on SIGHUP without a restart.