Privasys
Enclave OSEnclave OS (Virtual)

Hardened Guest Images

The dm-verity base image for Enclave OS Virtual, the trust chain from silicon to userland, and why minimal images matter for confidential computing.

Enclave OS Virtual boots from a minimal, read-only, fully measured VM image built with mkosi. Every byte of the root filesystem is verified by dm-verity against a hash tree whose root hash is measured by TDX into the RTMR registers. A remote verifier can check the RTMR values and know, cryptographically, that the VM is running exactly the code that was built from the Privasys cvm-images repository.

Trust Chain

Silicon (TDX hardware)
  └─ MRTD     measures the TD firmware (OVMF/TDVF) loaded by the hypervisor
      └─ RTMR[0]     measures the firmware configuration (CC MR 1)
          └─ RTMR[1]     measures the EFI boot path: shim and GRUB binaries (CC MR 2)
              └─ RTMR[2]     measures OS boot: kernel, initrd, cmdline with dm-verity root hash (CC MR 3)
                  └─ dm-verity     every block of the rootfs verified against the hash tree
                      └─ All userland binaries     any modification = I/O error + kernel panic

Every byte of code that executes on the machine is either measured by TDX hardware or verified by dm-verity. There are no gaps in the chain.

Measured Boot, Not Secure Boot

The images rely on measured boot (TDX RTMRs verified through remote attestation) rather than UEFI Secure Boot, and are deployed with Secure Boot disabled. This is deliberate:

  • Secure Boot with stock keys adds nothing for a verifier. The standard Microsoft/Canonical key chain accepts any generically signed OS, so a "Secure Boot enabled" bit proves nothing about which code booted. The RTMRs identify the exact shim, GRUB, kernel, command line, and dm-verity root hash, bit for bit.
  • Cloud independence. Enrolling custom Secure Boot keys is a cloud-specific mechanism (different on GCP, Azure, and bare metal). Measured boot works identically everywhere TDX or SEV-SNP is available, keeping the trust chain cloud-agnostic.
  • A hardened custom kernel cannot be Microsoft-signed without depending on an external review process. Measuring it instead keeps the supply chain entirely under Privasys control.

The security guarantee is unchanged: a verifier that checks the RTMR values gets a stronger statement than Secure Boot can make, because it learns exactly what booted instead of merely that something acceptably signed booted.

Why a Custom Image

General-purpose Confidential VM images (such as Google's pre-built Ubuntu images) are designed to run any workload. That generality is at odds with verifiability. A confidential VM is only as trustworthy as the code running inside it.

General-purpose CVM imagePrivasys cvm-images
Root filesystemext4 (read-write)erofs (read-only)
dm-verityNot enabledEnabled on every block
Installed packages~2000+ (full Ubuntu stack, cloud agents, package managers)~40 (minimal: kernel, systemd, attestation tools — no SSH, no debug tools in production)
Image size~30 GB~1.5 GB
Can modify rootfs at runtimeYesNo (I/O error then kernel panic)
Kernel modulesAll modules including unsigned third-party driversSigned modules only (module.sig_enforce=1)
Kernel lockdownNot enforcedlockdown=integrity (no unsigned code in ring 0)
Attack surfaceLarge: writable FS, package managers, update servicesMinimal: read-only FS, no package manager, no writable paths except tmpfs and data partition
What TDX attests"Some image the cloud provider built""This exact erofs image with this exact dm-verity root hash, bit-for-bit"

The core problem with general-purpose images:

  1. The rootfs is writable, so software can be installed or replaced after boot. The TDX measurement covers the initial state, but the running state can drift.
  2. Without dm-verity, nothing prevents a compromised process from modifying binaries on disk.
  3. Thousands of packages means a large attack surface.
  4. Unsigned kernel modules can be loaded, giving arbitrary code full ring-0 access to TEE-protected memory.

With cvm-images, the dm-verity root hash is baked into the kernel command line and measured by TDX. A remote verifier can check the RTMR values against the expected hash and confirm the VM is running exactly the code in the repository.

What Is in the Image

ComponentDetails
Guest OSUbuntu 24.04 LTS (Noble Numbat)
KernelCustom-built, ABI-pinned Ubuntu 6.17 kernel (CVM Guard hardening patches, TDX guest support), built and signed in CI
Root filesystemerofs (read-only)
Integritydm-verity hash tree
Bootshim then GRUB then kernel + initrd, each stage measured into the RTMRs; dm-verity root hash in cmdline
Boot integrityMeasured boot: every boot component recorded in the TDX RTMRs and checked via remote attestation (see above)
PartitionsESP (512 MB) + root erofs (~940 MB) + verity hash (~63 MB)
Networkingsystemd-networkd with DHCP
SSHNone: production images carry no sshd binary at all. The separate dev image profile adds openssh and debug tools, and is identifiable in the attestation.
Image profile marker/etc/privasys/image-profile (production or dev), part of the measured rootfs, surfaced in RA-TLS certificates as OID 1.3.6.1.4.1.65230.2.8
Attestation supporttpm2-tools, clevis, cryptsetup
Cloud integrationgoogle-guest-agent (dev profile adds google-compute-engine and OS Login; removable for other platforms)

erofs and dm-verity

erofs (Enhanced Read-Only File System) is a read-only filesystem designed for small size and fast random reads. It is ideal for dm-verity because writes are impossible by design, not just by policy. Any attempt to write to the root filesystem results in an immediate I/O error.

dm-verity is a kernel device-mapper target that provides transparent integrity checking. Every 4 KB block of the root partition has a corresponding SHA-256 hash in the verity hash partition. When a block is read, the kernel computes its hash and compares it against the stored value. If the hashes do not match, the read fails and the kernel panics.

Together, they guarantee that the root filesystem is bit-for-bit identical to what was built. No runtime modification is possible.

Where It Fits in the TDX Stack

┌─────────────────────────────────────────────────────────────┐
│  TDX SEAM module                          Silicon firmware  │
│  Creates and isolates Trust Domains, manages memory keys    │
└──────────────────────────┬──────────────────────────────────┘
                           │ creates / measures
┌──────────────────────────▼──────────────────────────────────┐
│  Host OS / hypervisor                                       │
│  Modified kernel, QEMU, OVMF/TDVF                           │
│  Enables the host to launch TDX guests                      │
└──────────────────────────┬──────────────────────────────────┘
                           │ launches
┌──────────────────────────▼──────────────────────────────────┐
│  cvm-images (tdx-base)                      Guest OS image  │
│  GRUB boot, erofs root, dm-verity, attestation tools        │
└──────────────────────────┬──────────────────────────────────┘
                           │ services go here
┌──────────────────────────▼──────────────────────────────────┐
│  Enclave OS Virtual                                         │
│  RA-TLS module, containerd, management server, containers   │
└─────────────────────────────────────────────────────────────┘

The guest OS image is the foundation. Application-specific layers (the RA-TLS module, containerd, the management server) are built on top of it.

Releases and Measurements

Each tagged release of the base image publishes:

ArtifactDescription
dm-verity root hashThe SHA-256 root of the hash tree covering every byte on the read-only root filesystem. This is the primary code identity measurement.
Predicted RTMR[1] / RTMR[2]The expected TDX measurement register values, computed from the artifact itself (GPT, shim/GRUB Authenticode digests, grub.cfg command sequence, kernel, command line, initrds) by replaying the SHA-384 extend chain — no boot required. A per-event digest manifest (.measurements.json) accompanies each artifact.
Disk imageBootable TDX Confidential VM image (cloud-specific formats produced by CI)

Because the expected values are derived from the published artifact rather than recorded from a "golden boot", a verifier can independently rebuild the measurement chain and confirm both the artifact and the attestation refer to exactly the same bytes. Build inputs are pinned (apt packages to a dated snapshot.ubuntu.com timestamp, the kernel to an exact ABI, the toolchain to commit SHAs) so rebuilds see identical inputs.

The dm-verity root hash is embedded in the kernel command line (roothash=...) and measured into RTMR[2] at boot (via CC MR 3). RTMR[1] (CC MR 2) measures the boot manager code (shim and GRUB binaries). Together, a remote verifier can confirm the exact bootloader AND root filesystem by checking RTMR[1] and RTMR[2] in the TDX quote.

RTMR Mapping

On Google Cloud's TDX implementation, the CC Event Log (CCEL) uses CC Measurement Register indices that map to TDX RTMRs:

CC MRRTMRTCG PCRsWhat it measures
1RTMR[0]PCR 0Firmware code (TDVF/OVMF)
2RTMR[1]PCR 1–7EFI boot path: shim binary, GRUB binary, Secure Boot variables, EFI actions
3RTMR[2]PCR 8–15OS boot: GRUB commands, kernel binary, initrd, kernel command line (including roothash=), MOK list
4RTMR[3]PCR 16+Application-defined (unused)

The dm-verity root hash appears in the kernel command line, which GRUB measures into CC MR 3 (RTMR[2]). RTMR[1] captures the bootloader binaries themselves, it changes when the shim or GRUB is updated, but not when the root filesystem changes.

Verifying RTMRs from the Event Log

You can independently verify that the RTMR values in a TDX quote match the CC Event Log by replaying the SHA-384 extend operations. Each RTMR starts at all zeros and is extended by each measurement event:

RTMR[i] = SHA-384( RTMR[i] ‖ event_digest )

Reading the event log

The CCEL event log is available inside the VM at /sys/firmware/acpi/tables/data/CCEL. Parse it and replay the extends:

# Install tpm2-tools (if not already present)
sudo apt-get install -y tpm2-tools

# Dump the event log in human-readable format
sudo tpm2_eventlog /sys/kernel/security/tpm0/binary_bios_measurements

Finding the dm-verity root hash

Search the event log for the kernel command line event. On a cvm-images image, it contains the dm-verity root hash:

sudo tpm2_eventlog /sys/kernel/security/tpm0/binary_bios_measurements \
  | grep -A2 "kernel_cmdline"
# Example output:
#   kernel_cmdline: /vmlinuz-6.17.0-35-generic root=PARTUUID=... roothash=b93b4ee5...

Replaying RTMR extends (Go)

The following Go program parses the CCEL event log, replays the SHA-384 extends, and prints the computed RTMR values. Compare the output with the RTMR values from the TDX attestation quote.

package main

import (
	"crypto/sha512"
	"encoding/binary"
	"encoding/hex"
	"fmt"
	"io"
	"os"
)

const (
	algSHA384  = 0x000c
	sha384Size = 48
	evNoAction = 0x00000003
)

// CC MR index → RTMR mapping (CCEL format)
func ccmrToRTMR(ccmr uint32) int {
	switch ccmr {
	case 1:
		return 0 // RTMR[0]
	case 2:
		return 1 // RTMR[1]
	case 3:
		return 2 // RTMR[2]
	case 4:
		return 3 // RTMR[3]
	default:
		return -1
	}
}

func main() {
	path := "/sys/firmware/acpi/tables/data/CCEL"
	if len(os.Args) > 1 {
		path = os.Args[1]
	}

	f, err := os.Open(path)
	if err != nil {
		fmt.Fprintf(os.Stderr, "open: %v\n", err)
		os.Exit(1)
	}
	defer f.Close()

	// Skip TCG_EfiSpecIDEventStruct header (legacy format)
	var pcr0 uint32
	var evType0 uint32
	var digest0 [20]byte
	var evSize0 uint32
	binary.Read(f, binary.LittleEndian, &pcr0)
	binary.Read(f, binary.LittleEndian, &evType0)
	io.ReadFull(f, digest0[:])
	binary.Read(f, binary.LittleEndian, &evSize0)
	headerData := make([]byte, evSize0)
	io.ReadFull(f, headerData)

	// Parse algorithm descriptors from SpecID header
	numAlgs := binary.LittleEndian.Uint32(headerData[24:28])
	type algDesc struct{ id, size uint16 }
	algs := make([]algDesc, numAlgs)
	for i, off := uint32(0), 28; i < numAlgs; i, off = i+1, off+4 {
		algs[i] = algDesc{
			binary.LittleEndian.Uint16(headerData[off:]),
			binary.LittleEndian.Uint16(headerData[off+2:]),
		}
	}

	// Replay extends
	var rtmr [4][sha384Size]byte

	for evNum := 1; ; evNum++ {
		var ccmr, eventType, digestCount uint32
		if binary.Read(f, binary.LittleEndian, &ccmr) != nil {
			break
		}
		binary.Read(f, binary.LittleEndian, &eventType)
		binary.Read(f, binary.LittleEndian, &digestCount)

		var sha384Digest [sha384Size]byte
		for i := uint32(0); i < digestCount; i++ {
			var algID uint16
			binary.Read(f, binary.LittleEndian, &algID)
			if algID == 0xffff {
				goto done
			}
			var size uint16
			for _, a := range algs {
				if a.id == algID {
					size = a.size
					break
				}
			}
			digest := make([]byte, size)
			io.ReadFull(f, digest)
			if algID == algSHA384 {
				copy(sha384Digest[:], digest)
			}
		}

		var evDataSize uint32
		binary.Read(f, binary.LittleEndian, &evDataSize)
		evData := make([]byte, evDataSize)
		io.ReadFull(f, evData)

		if eventType == evNoAction {
			continue
		}
		idx := ccmrToRTMR(ccmr)
		if idx < 0 || idx > 3 {
			continue
		}
		h := sha512.New384()
		h.Write(rtmr[idx][:])
		h.Write(sha384Digest[:])
		copy(rtmr[idx][:], h.Sum(nil))
	}
done:
	for i := 0; i < 4; i++ {
		fmt.Printf("RTMR[%d] = %s\n", i, hex.EncodeToString(rtmr[i][:]))
	}
}

Run it on the VM and compare with the attestation quote:

go build -o rtmr-replay . && sudo ./rtmr-replay
# RTMR[0] = 75d9f1d1...
# RTMR[1] = 9cc6d43f...
# RTMR[2] = c8a9e472...   ← contains the dm-verity root hash measurement
# RTMR[3] = 000000000...

If the computed values match the RTMR values in the TDX attestation quote, the event log is authentic and the boot chain is verified.

Letting the attestation server do the replay

The Privasys attestation server performs the same cross-check on the server side. Send the quote together with the base64-encoded CCEL:

curl -X POST https://as.privasys.org/ \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"quote": "<base64 quote>", "eventLog": "<base64 CCEL>", "includeEventLog": true}'

The verification fails closed: if the replayed registers do not equal the quote's RTMRs, the response is VERIFICATION_FAILED naming the mismatching register. On success, eventLogVerified is true and includeEventLog returns each decoded event (register, type, digest, and printable payloads such as grub_cmd lines), which you can compare entry by entry against the measurements.json published with every cvm-images release to pinpoint exactly which boot component changed.

How Updates Work

The rootfs is read-only. Running apt install on a booted VM is impossible. To update:

  1. Edit the configuration in the cvm-images repository (add or update packages, bump the image version).
  2. Build the image with sudo mkosi build.
  3. Test locally with QEMU.
  4. Upload and register the new image on the target cloud platform.
  5. Create a new VM, attach the existing data disk, and delete the old VM.

The data partition (LUKS-encrypted, separate persistent disk) survives image updates.

Production and Dev Profiles

Every image is built in two flavors from the same configuration:

ProductionDev
SSHNo sshd binary on diskopenssh-server (key-based, via OS Login)
Debug toolsNonestrace, tcpdump, curl, jq
Profile marker/etc/privasys/image-profile = production/etc/privasys/image-profile = dev

The marker lives on the dm-verity-protected rootfs, so it cannot be changed at runtime and a forged value would change the measurement. The RA-TLS certificate carries it as the Image Profile extension (OID 1.3.6.1.4.1.65230.2.8); verification libraries reject dev images unless explicitly opted in.

Design Choices

DecisionRationale
erofs over ext4Read-only by design. Smaller than ext4, ideal for dm-verity. No accidental writes possible.
Measured boot over UEFI Secure BootStock Secure Boot keys accept any generically signed OS and custom-key enrollment is cloud-specific. Attested RTMRs identify the exact boot chain on any cloud.
GRUB over systemd-bootGRUB measures its configuration, the kernel, and the command line (including the dm-verity root hash) into the RTMRs. It is the proven measured-boot chain for TDX across cloud platforms.
Minimal package setEvery additional package increases the attack surface. ~40 packages means fewer potential CVEs than ~2000.
Signed kernel modules onlymodule.sig_enforce=1 plus lockdown=integrity prevents unsigned code from running in ring 0. Out-of-tree modules (e.g. NVIDIA) are signed in CI with an ephemeral key that is destroyed after the build.
Production/dev image splitDebugging needs SSH; production must not have it. Two profiles of the same image, distinguishable in the attestation, instead of one compromise image.
Edit on GitHub