Skip to main content

Encrypted /var

/var is the only writable partition on an Avocado OS device: extensions, application data, Docker layers, and device state all live there. runtimes.<name>.var.encrypt turns it into a LUKS2 container. Where the board has a hardware key store the key is bound to it; var.hardware chooses the engine, and auto falls back to a derived key where none works (see Choosing the key engine).

The root filesystem is not encrypted — its contents are the OS, which is public. What matters there is integrity, which dm-verity provides when you opt in with rootfs.image.verity, on targets that can carry the root hash in a boot FIT.

Enable it​

avocado.yaml
runtimes:
prod:
var:
encrypt: true
Host machine
avocado build
avocado provision -r prod --profile sd

That is the whole opt-in. The CLI does the rest:

  • adds cryptsetup-var to the initramfs package set and cryptsetup-var-udev to the rootfs package set,
  • writes an /etc/avocado/var-encrypt marker into this runtime's initramfs.

cryptsetup-var.service is conditioned on that marker, so a feed image or a runtime that did not opt in never encrypts /var: a partition that is still plaintext stays plaintext, and one that is already LUKS stays LUKS (see the note below). With the marker, the first boot encrypts the flashed var image in place — seeded content (subvolumes, var_files, primed Docker images) survives — and later boots open it as /dev/mapper/var.

Leaving var.encrypt unset produces a byte-identical image to before.

This can ship as an OTA

Enabling var.encrypt works through a normal OS update: the first boot of the new slot encrypts the flashed /var in place. Turning it back off afterwards does not — the partition stays LUKS and /var will not mount. Reprovision instead.

Choosing the key engine​

avocado.yaml
runtimes:
prod:
var:
encrypt: true
hardware: tpm2
ValueBehavior
auto (default)Enroll whatever engine the machine ships and probes successfully; degrade to the derived key and report it in the posture.
caamThe NXP CAAM must hold a keyslot. If it cannot, the boot fails to the emergency target rather than opening /var on the derived key.
tpm2Same contract, for a TPM 2.0 or an OP-TEE fTPM.
noneNo hardware keyslot at all. Refused unless var.recovery is also set — otherwise nothing would hold a key.

caam and tpm2 fail closed in all three ways this could otherwise slip: the engine is unusable at preflight, an existing hardware keyslot will not unlock (wiped key store, moved PCRs), or enrollment itself fails. The single exception is the first-enrollment boot — before any hardware token exists, the derived key is the only thing luksAddKey can authenticate with, so that one open is allowed. The carve-out keys on the absence of the token, not on the mode.

An explicit choice rides in the initramfs as /etc/avocado/var-hardware, next to the var-encrypt marker. auto writes nothing.

What binds the key on each platform​

PlatformFirst bootLater boots
i.MX 8MArgon2id key from the SoC UID, plus a keyslot from a CAAM black keyCAAM-derived passphrase, Argon2id fallback
i.MX 9Argon2id key from the SoC UIDsame key (ELE backend pending)
JetsonArgon2id key, plus a TPM2 keyslot sealed to the OP-TEE fTPM (PCR 7)TPM2 token, Argon2id fallback
qemu, x86Argon2id, plus swtpm/TPM 2.0 where presentas Jetson

The hardware blob is stored in the LUKS2 header as an avocado-hwkey token — no extra partition, and it travels with the container.

Operator recovery key​

Every keyslot described so far is bound to the device: if the SoC dies, /var dies with it. var.recovery is the exception. It names a master secret you hold, from which each unit's passphrase is derived. Nothing derived from the master enters a build, and nothing but the resulting keyslot is on the device.

1. Create the master​

Host machine
avocado signing-keys create fleet-var-master --algorithm hmac-sha256

This is a new secret key kind in the signing-key registry: 32 random bytes, stored 0600. It lives outside the signing-keys directory the SDK bind-mounts into build containers, so no build hook can read it. A master found in the old location is refused with the commands to move it, rather than migrated automatically — the move is a rename plus a registry rewrite, and a failure between the two would lose the key that opens a fleet's /var.

Back this up off the build host

Losing the master means every unit that has retired its derived keyslot is unrecoverable.

2. Name it in the runtime​

avocado.yaml
runtimes:
prod:
var:
encrypt: true
recovery: fleet-var-master

3. Enroll a device​

Host machine
avocado var-key enroll prod --device root@192.168.1.80

enroll reads the device's SoC UID over SSH — the device tree serial-number, then soc0/serial_number, the same sources the initramfs uses — derives that unit's passphrase as HMAC-SHA256(master, "avocado-var-recovery\0" || UID), and pipes it to avocadoctl var-key enroll on the device, which adds it as a LUKS2 keyslot carrying an avocado-recovery token.

Once a recovery token exists, the initrd retires the SoC-UID-derived keyslot — the one key that anyone who can read the UID could reproduce — unless that key is what opened /var this boot.

4. Recover a unit on the bench​

Host machine
avocado var-key derive prod --uid 0x0123456789abcdef --raw \
| cryptsetup open --key-file - /dev/disk/by-partlabel/var var

--raw emits the 32 raw bytes for cryptsetup --key-file -; without it you get hex.

How the keyslot change is authorized​

None of the passphrases that open /var are reachable from the running system — that is the point. So cryptsetup-var links the volume key into root's user keyring on every open (--link-vk-to-keyring @u::%user:cryptsetup:var) and avocadoctl var-key authorizes with luksAddKey --volume-key-keyring.

On the device​

Target device
avocadoctl var-key list # keyslots, and what unlocks each
avocadoctl var-key enroll # passphrase on stdin, or --key-file
avocadoctl var-key remove --yes # drop the recovery keyslot

On machines that boot U-Boot, encryption posture is published into the device's U-Boot environment block, so read it with fw_printenv:

Target device
fw_printenv avocado_var_encrypted avocado_var_unlock avocado_var_tpm2_token avocado_var_hwkey avocado_var_recovery

Jetson boots the NVIDIA chain and has no U-Boot environment to publish into, so the keys below are not available there. Use avocadoctl var-key list and the journal instead.

KeyMeaning
avocado_var_encryptedWhether /var came up as a LUKS container this boot
avocado_var_unlockHow the keyslot was opened this boot (e.g. tpm2, argon2id)
avocado_var_tpm2_tokenWhether a TPM2 keyslot exists, independent of which one actually opened
avocado_var_hwkeyName of the hardware backend in use, or no
avocado_var_recoverykey (operator slot enrolled) or soc-uid (still on the derived slot)

These are facts the initramfs observed that userspace cannot reconstruct. They are written only when a value changes, since the backing store is a U-Boot environment block and re-writing on every boot would be flash wear for no new information — a change is also the interesting event, being the first enrollment or the day a device silently dropped to the recovery slot. A device that has a hardware keyslot but opened without it is called out explicitly, so a fleet-wide degrade is visible rather than silent.

imx93-frdm with verified-boot

On avocado-imx93-frdm built with the verified-boot feature, U-Boot's env permit list does not yet carry avocado_var_unlock, avocado_var_tpm2_token, or avocado_var_encrypted. A key absent from that list is silently discarded on the next OTA slot switch rather than merely ignored, so those three keys do not survive past the first update on this specific board/feature combination. avocado_var_hwkey and avocado_var_recovery are unaffected everywhere.

On an image built without /var encryption the reporter is a no-op and publishes nothing.

Requirements for a board​

A machine must have a dm-crypt kernel fragment reachable from its kernel recipe and a GPT var partlabel before it can declare encrypted-var. Raspberry Pi has neither today. See Security features for the current board matrix.

On a fresh flash the var partition is grown to fill its disk before the container is opened, so the LUKS mapping and the filesystem are full size from the first boot and the first OTA has room.