Skip to content

cryptsetup-var: report /var encryption posture to the KV store - #295

Closed
jetm wants to merge 3 commits into
wrynosefrom
imx93-var-posture-reporting
Closed

jetm wants to merge 3 commits into
wrynosefrom
imx93-var-posture-reporting

Conversation

@jetm

@jetm jetm commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Problem

cryptsetup-var.sh fails open in two places, both deliberately. ensure_tpm2_enroll() keeps the volume encrypted under Argon2id if the TPM2 enroll fails, and open_var() falls back to the Argon2id key file if the TPM2 unseal fails. That is the right call — a legitimate PCR 7 change (a Secure Boot policy update, a key rotation) must not lock a device out of its own /var.

The cost of that choice is silence. A device whose PCR 7 moved keeps booting on the recovery slot indefinitely, and from outside it is indistinguishable from a correctly sealed one: a later luksDump shows that a TPM2 token exists, not that it was the thing that worked. At fleet scale that means a storage-security control can lapse across a whole deployment with no signal anywhere.

Solution

Record the one fact nothing can reconstruct afterwards — which keyslot actually opened the volume — and publish it, plus the token's presence and whether /var really came up through the mapper, into the U-Boot KV store that peridiod already reads.

The /var-through-the-mapper check is re-done in userspace rather than trusted from the initrd on purpose: opening the container says nothing about what fstab then mounted. That exact mismatch was a real defect on this board when AVOCADO_VAR_PART_DEV was unset — container opened in the initrd, /var mounted from the raw partition underneath it.

Key changes

  • cryptsetup-var.sh gains write_posture(), which drops three facts in /run/avocado-var-posture. Best-effort by construction: posture must never be the reason /var fails to open.
  • New avocado-posture-publish.sh + .service (rootfs-only) read that file, re-check the mount, and publish avocado_var_unlock, avocado_var_tpm2_token, avocado_var_encrypted.
  • New ${PN}-posture package so the publisher stays out of the initrd.

Two design points worth calling out for review:

Why /run and not the KV store directly from the initrd. Neither fw_setenv nor the fw_env.config it needs exists in this initramfs (see the recipe's own RDEPENDS note), and avocado-uboot-env.service generates that config in the real root. /run is the same early tmpfs the key file already depends on, and systemd carries it across the switch-root.

Why publish on change only. The KV backend is a U-Boot environment block on the boot medium and its own docs expect infrequent writes. Re-writing three keys every boot would be steady flash wear for no new information, and a change is the interesting event — the first enroll, or the day a device silently drops to the recovery slot.

The unit orders After=avocado-uboot-env.service without requiring it: that unit lives in meta-avocado-nxp while this ships in the shared layer, so it has to stay valid on a machine with no such generator. systemd ignores After= on a non-existent unit, and the script exits 0 when the tooling is absent.

Verification

Not built and not run on hardware — the build hosts were in use. What was verified, by stubbing fw_printenv/fw_setenv/findmnt:

Case Result
Healthy first boot publishes all three keys
Identical second boot writes nothing (write-on-change holds)
tpm2 -> argon2id regression flips only that key, warns naming PCR 7
/var on the raw partition reports avocado_var_encrypted=no
No posture file (unencrypted image) exits 0 with a diagnostic
No libubootenv tools exits 0 with a diagnostic

Separately confirmed write_posture cannot abort the unlock: it survives a failing token probe under set -e (which is why it uses if rather than && — the && form is the footgun here) and survives an unwritable target. shellcheck --shell=sh is clean on both scripts.

Reviewer notes

Three things I could not verify without a build, listed so they get eyes rather than assumed:

  1. The package split depends on PACKAGES order. FILES:${PN} globs the whole ${libexecdir}/cryptsetup-var/ directory and so also matches the publisher; PACKAGES =+ prepends, so ${PN}-posture claims it first and it stays out of the initrd. I documented that in the recipe, but documenting is not testing — worth confirming the publisher is genuinely absent from the initramfs after a build.
  2. libubootenv and util-linux-findmnt resolving as RDEPENDS.
  3. The transport stops at the KV store. peridiod reads that store, but I did not verify it forwards arbitrary avocado_* keys to Peridio cloud, or that anything cloud-side surfaces them. So this makes posture durable and queryable on-device; the fleet-visible half is a further step.

Scope stayed at LUKS/TPM deliberately. No AHAB lifecycle or fuse-state reporting here — I have no verified userspace path to read the ELE lifecycle and would have been guessing at one.

@jetm
jetm force-pushed the imx93-var-posture-reporting branch 2 times, most recently from a467c98 to b66bddb Compare August 20, 2026 15:18
@jetm
jetm marked this pull request as draft August 20, 2026 22:05
@mobileoverlord
mobileoverlord force-pushed the imx93-secure-boot-wrynose branch from 9308e29 to 5b7f642 Compare August 24, 2026 20:34
@jetm
jetm force-pushed the imx93-secure-boot-wrynose branch from 61c163c to a2efa39 Compare August 24, 2026 23:35
Base automatically changed from imx93-secure-boot-wrynose to wrynose August 24, 2026 23:48
@jetm
jetm force-pushed the imx93-var-posture-reporting branch from ac87d71 to 09bb9a0 Compare August 25, 2026 16:20
jetm added 3 commits August 26, 2026 13:08
Both the TPM2 enroll and the TPM2 open in cryptsetup-var.sh fail open
on purpose, because a legitimate PCR 7 change must not lock a device
out of its own /var. The cost of that choice is silence: a device
whose PCR 7 moved keeps booting on the Argon2id recovery slot
indefinitely, and from outside it is indistinguishable from a
correctly sealed one. At fleet scale that is a security control that
can lapse across a whole deployment with no signal anywhere.

Record which keyslot actually opened the volume, because that is the
one fact nothing can reconstruct later - a luksDump afterwards shows
that a TPM2 token exists, not that it was what worked. Pair it with
the token's presence and whether /var really came up through the
mapper, the latter re-checked in userspace rather than trusted from
the initrd because opening the container says nothing about what
fstab then mounted; that exact mismatch was a real defect on this
board when AVOCADO_VAR_PART_DEV was unset.

The initramfs cannot write the KV store directly - neither fw_setenv
nor the fw_env.config it needs exists there, and the generator for
that config runs in the real root. So the initrd drops the facts in
/run, the tmpfs the key file already depends on and which systemd
carries across the switch-root, and a rootfs-only unit publishes from
there into the U-Boot environment peridiod already reads. Publishing
on change only respects that store's documented expectation of
infrequent writes, and makes the write itself the interesting event
rather than steady per-boot flash wear.

Verified by stubbing fw_printenv/fw_setenv/findmnt over six cases:
first boot publishes all three keys, an identical second boot writes
nothing, a tpm2 -> argon2id regression flips only that key and emits
a warning naming PCR 7, a /var mounted off the raw partition reports
unencrypted, and an absent posture file or absent libubootenv both
exit 0 with a diagnostic. Separately confirmed write_posture cannot
abort the unlock: it survives a failing token probe under set -e (the
reason it uses `if` rather than `&&`) and an unwritable target.

Not yet built or run on hardware - the build hosts were in use.

Signed-off-by: Javier Tia <javier@peridio.com>
cryptsetup-var-posture was defined, packaged and set to auto-enable its
unit, but nothing in the tree ever pulled it into an image. The RPM built
and stopped there: a qemuarm64 encrypted-var rootfs came out with
cryptsetup-var-udev in its manifest and no posture package, so
avocado-posture-publish.service never reached a device and the posture this
branch exists to report was never published anywhere.

Add it beside cryptsetup-var-udev in the same encrypted-var branch, which
is the condition that already decides whether any of this content is wanted
- the posture reporter has nothing to describe on a machine whose /var is
not a dm-crypt mapping.

The auto-enable was what hid this. A package with SYSTEMD_AUTO_ENABLE looks
wired up on inspection, and the unit is genuinely correct; the gap was one
level up, in whether anything installed it. Nothing failed, nothing warned,
and the built RPM in deploy/rpm made it look present.

Signed-off-by: Javier Tia <javier@peridio.com>
The posture keys are written from Linux with fw_setenv and read back with
fw_printenv, so nothing in this script's own path suggests U-Boot has an
opinion about them. On a machine built with CONFIG_ENV_WRITEABLE_LIST it does,
and the failure is silent: env_flags_validate rejects an H_EXTERNAL variable
without the 'w' flag so it never reaches env_htab, env_export serialises the
hashtable, and the A/B flow calls saveenv on every slot switch. A key absent
from CFG_ENV_FLAGS_LIST_STATIC therefore survives until the first OTA and is
then gone, with nothing logged and a fleet view that keeps reporting whatever
it last saw.

Neither this change nor the one adding that permit list is wrong on its own,
which is why no review of either would surface it. Recording it against the
keys themselves puts the constraint where the next person adds a fourth key,
rather than in a commit message nobody reads at that moment.

The debt marker rather than the fix because the permit list has not merged
yet, so there is no list here to add the entries to. It names the trigger so
this cannot leave draft without them.

Also note the consequence for consumers: a permitted key is writable from the
saved environment by construction, so posture is forgeable by anyone who can
write the boot medium. That is acceptable for an observation, and is exactly
why it must never be presented as tamper-evident.

Signed-off-by: Javier Tia <javier@peridio.com>
@mobileoverlord

Copy link
Copy Markdown
Contributor

Merged into wrynose via #316 (rebased, commits preserved). Closing as included.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants