Skip to content

Volume encryption

A persistent volume holds a stateful workload's real data — a Postgres cluster, a Redis dump, a user's files. That data lives in a backing file on the host disk. Volume encryption makes each volume its own LUKS2 container with its own key, so the backing file is ciphertext at rest and its data can be crypto-shredded by destroying that one key.

This is the standard encryption-at-rest model (the same one AWS EBS gives you): it protects a stolen, seized, RMA'd, or decommissioned disk, and it gives you a key you control and can revoke. See What it protects for the honest boundary — it is not protection against a compromised host.

Enable it

Encryption is opt-in: the daemon needs a master key. Point it at a key file (generated on first use) or supply the key in the environment:

crucible daemon --volume-dir /var/lib/crucible/volumes \
                  --volume-encrypt-key-file /var/lib/crucible/volume.key

# or, key from a KMS / systemd credential (takes precedence over the file):
CRUCIBLE_VOLUME_KEY="$(cat key.b64)" crucible daemon --volume-dir

Add --volume-encrypt to make every new volume encrypted by default; without it, encryption is available but a volume is only encrypted when you ask (--encrypt). With no key configured, encryption is off and --encrypt / volume shred are rejected — there is no silent fallback.

⚠️ Back up the master key. It is the only thing that can open your encrypted volumes — losing it means losing all of their data. The key lives outside the volume store (a key file, CRUCIBLE_VOLUME_KEY, or a KMS), so a stolen disk alone is inert. Keep it somewhere safe and separate, and treat it like the database it unlocks.

The volume key is independent of the secrets master key — point both flags at the same file if you want one key, or keep them separate.

Create an encrypted volume

# Explicitly encrypted, regardless of the daemon default:
crucible volume create pgdata --encrypt --size 10G

# Explicitly plaintext, even when --volume-encrypt is the daemon default:
crucible volume create cache --no-encrypt

# `volume ls` shows which volumes are encrypted:
crucible volume ls
# NAME     SIZE   ENCRYPTED  ATTACHED  HOST   AGE
# pgdata   10G    yes        -         host1  2m
# cache    2G     no         -         host1  1m

Attach it to an app or sandbox exactly like any volume — the encryption is transparent to the guest:

crucible app create db --image postgres:16-alpine \
  --volume pgdata:/var/lib/postgresql/data \
  --env POSTGRES_PASSWORD=… --env PGDATA=/var/lib/postgresql/data/pgdata

How it works

Each encrypted volume is a LUKS2 container (aes-xts-plain64, a 512-bit key — AES-256-XTS, the disk-encryption standard) over its backing file. A fresh random per-volume key unlocks it; that key is sealed under the daemon master key (AES-256-GCM, bound to the volume name) and stored in the volume record — never in the clear on disk. The record's wrapped key is the only copy.

When the volume attaches, the daemon opens the LUKS container to a decrypted device (/dev/mapper/crucible-vol-<name>) and — under the jailer — stages that device node into the VM's chroot (the same mechanism the jailer uses for /dev/kvm), never the ciphertext file. The guest sees an ordinary block device and mounts plaintext; the bytes on disk are ciphertext. Encryption and decryption happen in the kernel's device-mapper layer, so it is transparent — including to the snapshot/wake memory pager, which pays no per-page cost.

Because the guest key is a high-entropy random value, the LUKS keyslot uses a fast KDF: opening a volume (on attach, and on every scale-to-zero wake) stays sub-second.

Encrypted at rest, including while asleep

A scale-to-zero app snapshots and stops its VM when idle. For an encrypted volume, the daemon closes the decrypted device on sleep (the single-writer claim is kept) and re-opens it on wake — so a slept database's data is ciphertext at rest for the entire sleep, not left decrypted on the host.

Backups

A backup of an encrypted volume is the ciphertext container, and the backup record carries the volume's wrapped key — so it is inert without the master key, and a restore re-wraps that key under the new volume's name and brings the data back:

crucible app sleep db                 # quiesce (or detach) first
crucible volume backup pgdata
crucible volume restore --from <backup-id> --to pgdata-copy   # also encrypted

volume clone of an encrypted volume is not yet supported (an independent clone needs a fresh key — a full re-encrypt); restore a backup instead.

Crypto-shred

Deleting an encrypted volume's key makes its data permanently unrecoverable without touching the ciphertext blocks — instant, and provable:

crucible volume shred pgdata     # destroys the keyslots + deletes the wrapped key

shred is refused while the volume is attached to a live sandbox, and on a plaintext volume (use volume rm). It is gated by the default-deny delete scoped-token op. When several volumes share one key, destroying that key crypto-shreds all of them at once.

Multiple keys & rotation

The daemon can hold a keyring of more than one key, so different volumes can be wrapped under different keys — and any key can be rotated without re-encrypting a single byte of data.

Keys are loaded from, in addition to the default key:

# additional keys off-disk (preferred):
CRUCIBLE_VOLUME_KEY_archive="$(cat archive.key.b64)" crucible daemon

# or as files under a directory (id = filename without .key):
crucible daemon --volume-key-dir /etc/crucible/volume-keys   # 2026q3.key → id "2026q3"

--volume-default-key <id> chooses which key wraps new volumes (default default); volume create --encrypt uses it, and a create can name another key. volume ls shows each volume's KEY:

NAME     SIZE   ENCRYPTED  KEY       ATTACHED  HOST   AGE
pgdata   10G    yes        default   -         host1  5m
appdata  5G     yes        2026q3    -         host1  1m

Rotate a key — no data is re-encrypted

Rotation re-wraps a volume's per-volume key under a different keyring key. The key that actually encrypts the data (the LUKS volume key) never changes, so there is no cryptsetup run, no data movement, and no downtime — it is safe on a live, attached volume:

crucible volume rewrap pgdata --to-key 2026q3      # one volume
crucible volume rewrap --all --from-key default --to-key 2026q3   # every volume on a key

(Re-encrypting the underlying data — needed only if a data key is compromised — is a separate, expensive cryptsetup reencrypt procedure, not part of rewrap.)

Add or retire a key without a restart

Drop a new key into the env/dir, then reload the keyring in place:

crucible volume keys reload

To retire an old key: rewrap --all --from-key <old> --to-key <new>, remove the old key's file/env, then keys reload. Reload refuses to drop a key that any volume still uses (and a volume whose key is missing fails to open with a clear error), so you can't strand data. Rewrap and reload are gated by the default-deny volume_key scoped-token op.

Audit

Every key operation — volume_key_created, volume_key_rotated (with the from/to key ids), and volume_shredded — is emitted as a structured log record (and over OTLP when configured). The records carry volume names and key ids only, never key material.

What it protects (and what it doesn't)

Threat Protected?
Stolen / seized / RMA'd / decommissioned disk ✅ ciphertext at rest
Crypto-shred a volume (or destroy a shared key) ✅ data unrecoverable, immediately
One guest reading another's volume ✅ single-writer + separate keys + isolation
A compromised host root ❌ it holds the live device-mapper (the AWS-EBS model)
The guest itself ❌ it legitimately mounts plaintext

Closing the host-root gap needs confidential computing (encrypted guest RAM), which Firecracker does not support and which is fundamentally incompatible with the lazy-paging that powers sub-second scale-to-zero wake. So the honest claim is "encryption at rest with per-volume keys and crypto-shred," never "we can't read your data."

Slept apps and the memory snapshot

Volume encryption covers a workload's data on disk. There is one more surface to understand for a slept scale-to-zero app, and it's worth being precise about because it's where crucible differs from a stateless serverless-database design.

When crucible sleeps an app it snapshots the whole VM, including its RAM, to a memory file under --work-base, so it can wake warm in ~170 ms — buffers hot, no reconnect, no cold query. That snapshot can contain cached rows (a database's shared_buffers), so the memory file is at-rest data too. A stateless design instead discards RAM on suspend and reconnects to storage on wake — no memory file, but a cold wake that re-warms its buffers on the first queries. crucible trades that one extra surface for a much faster wake.

So there are two honest ways to keep a slept database encrypted at rest, and you choose per the tradeoff you want:

  • Warm (default): keep snapshot-wake, and put --work-base on an encrypted filesystem (LUKS / dm-crypt). Because device-mapper encryption is transparent — the kernel decrypts pages as the snapshot pager faults them in — this adds no per-page wake cost, and every byte the daemon writes (the memory file, the writable rootfs, the volume containers) is ciphertext at rest. This is the recommended setup.
  • Cold: a stop/start sleep that never writes RAM to disk — zero memory-at-rest surface, at the cost of a cold wake.

ℹ️ The daemon does not encrypt the memory file itself: doing so would force the wake path to decrypt every faulted page, taxing exactly the sub-second wake that scale-to-zero depends on. An encrypted --work-base gets the same result for free — the kernel does the crypto below the pager.

Startup advisory

When volume encryption is enabled but the daemon can positively see that --work-base sits on unencrypted storage, it logs a warning at startup:

volume encryption is on but --work-base is on unencrypted storage: a slept app's
memory snapshot (cached rows, buffers) is written to disk in the clear. Put
--work-base on a dm-crypt/LUKS filesystem for full encryption at rest.

It stays silent when --work-base is on an encrypted or ephemeral (tmpfs) filesystem, or when the backing storage can't be classified — so a warning means there is really something to fix.

Was this page helpful?