> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tinfoil.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Encrypted persistent volumes

> Extend the secure enclave's memory encryption to disk with volumes encrypted under a key only you hold.

<Note>
  Tinfoil offers two ways to persist data. **Encrypted volumes** (this page) are block storage:
  a filesystem attached to one enclave at a time, suited to large working sets such as build
  trees, datasets, and fine-tuning checkpoints, encrypted under a secret the deployment owner
  holds. The [buckets sidecar](/containers/persistent-storage) is object storage: encrypted
  objects in S3, reachable from any number of enclaves and clients, encrypted under keys your
  end users hold.
</Note>

## Overview

A secure enclave encrypts its memory with a key that only the enclave's own hardware can
use. That is what guarantees no operator, including Tinfoil, can read the data being processed
inside it. Memory, however, is not persistent, and even if it were, the key protecting it lives
in hardware registers that cannot be read out or saved. Everything in the enclave is lost when
it stops.

Encrypted persistent volumes extend the enclave model to storage. They compose two primitives:
[private secret delivery](/containers/private-secrets) and authenticated disk
encryption. A volume's contents are unreadable to anyone who does not hold its volume secret, and that
secret never exists outside your infrastructure and the enclave.

```mermaid theme={"dark"}
flowchart LR
    client["Your client<br/>(generates and stores the volume secret)"]
    subgraph enclave["Secure enclave"]
        boot["Boot"] -->|derives cipher and MAC keys| dm["dm-crypt + dm-integrity"]
        dm --> app["Your container<br/>/workspace"]
    end
    disk[("Persistent disk")]
    client -->|volume secret, released after attestation| boot
    boot ~~~ disk
    dm <-->|ciphertext only| disk
```

## Key management

Persistent storage needs a long-term secret to encrypt and authenticate data under. That secret cannot be managed
by Tinfoil, since anyone holding it can read or modify the data. It is created and kept on your side.

When you first provision a deployment with a volume, your client generates a random 64-byte
**volume secret** and stores it durably, either locally or in a keyserver on your own
infrastructure. The enclave never generates or persists this secret.

On every boot, the volume secret is delivered to the enclave after attestation, over a connection that
terminates inside the enclave. There are two ways to do this:

* **Automatically**, by declaring the volume secret as a private secret and letting your
  [keyserver](/containers/private-secrets) release it once the enclave's attestation matches your
  policy.
* **Manually**, by sending the secret over the attested connection to the running enclave, which
  unlocks the volume at runtime.

Inside the enclave, the volume secret is expanded with HKDF into the two keys the disk layer
needs: a 64-byte AES-XTS cipher key and a 32-byte HMAC key. You provision one secret; the
derived keys exist only in enclave memory.

<Warning>
  The volume secret is the confidentiality boundary for everything on the volume. If it is lost,
  the data cannot be recovered by you, by Tinfoil, or by anyone else.
</Warning>

## Disk encryption

Once the enclave holds the derived keys, it opens the volume with two Linux kernel primitives,
`dm-crypt` and `dm-integrity`, configured for authenticated encryption:

* **Confidentiality**: AES-XTS with a fresh random IV for every write, so identical plaintext
  produces different ciphertext each time it is written, regardless of where it lands on disk.
* **Integrity**: an HMAC-SHA256 tag over every 4 KiB sector. The tag and the IV are stored in
  a journaled `dm-integrity` layer beneath the encrypted device, and any sector whose tag does
  not verify is rejected on read.

Your container sees an ordinary writable filesystem. The disk, and anyone with access to it,
sees only ciphertext, tags, and IVs.

## Threat model

The goal is to give the volume the same guarantees as the enclave's encrypted memory, and to be
explicit about what it does not cover.

**Protected.** An attacker with full access to the disk cannot read its contents, and cannot
forge or alter a sector's contents without the enclave detecting it. Because every write uses a random IV, the
attacker also cannot tell whether two sectors hold the same data or whether a sector was
rewritten with its previous contents, which rules out ciphertext-comparison attacks.

**Not protected: rollback.** Integrity protection guarantees the ciphertext has not been
tampered with, but an attacker who replays an earlier, valid version of a sector, or restores
the whole disk to an earlier state, is not detected. This mirrors the memory model, where the
hardware protects against modification but not replay.

**Not protected: access patterns.** A privileged attacker can observe which sectors the enclave
reads and writes, and when. This side channel can leak information about enclave activity even
though the contents are encrypted. It can be closed at the application level by running an
Oblivious RAM (ORAM) scheme on top of the volume, at a performance cost that depends on your
workload. For an example of such implementation and a detailed study of this tradeoff, see
[Opal](https://arxiv.org/abs/2604.02522), a private memory system for personal AI
built on Tinfoil.

## Configuration

Declare volumes at the top level of `tinfoil-config.yml` and mount them into containers by name.
`key-secret` names the private secret that carries the volume secret; omit it to unlock the
volume manually at runtime instead. It must be a private secret released by your keyserver. A
Tinfoil-managed secret would give Tinfoil the ability to read the volume.

```yaml tinfoil-config.yml theme={"dark"}
cvm-version: "0.14.4"

keyserver-url: https://keys.example.com

volumes:
  - name: workspace
    size: 100GiB
    key-secret: WORKSPACE_SECRET

containers:
  - name: app
    image: "ghcr.io/myorg/app@sha256:<digest>"
    volumes: [workspace:/workspace]
```

| Field        | Type    | Required | Description                                                                                                  |
| ------------ | ------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `name`       | string  | Yes      | Volume name, referenced from `containers[].volumes` as `<name>:<mount path>`                                 |
| `size`       | string  | No       | Disk capacity, such as `100GiB`. Allocated once on first launch; changing it later does not resize the disk. |
| `key-secret` | string  | No       | Name of the private secret holding the base64-encoded 64-byte volume secret                                  |
| `owner`      | integer | No       | UID and GID that own the volume root                                                                         |
| `exec`       | bool    | No       | Allow executables on the volume                                                                              |

Requires `cvm-version` 0.14.4 or later.

<Note>
  Volumes persist across restarts and redeploys of the same deployment. Docker's writable
  layers, pulled images, and ordinary Docker volumes still live in memory and are lost on
  restart; only data written to a declared volume is durable.
</Note>
