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 panicEvery 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 image | Privasys cvm-images | |
|---|---|---|
| Root filesystem | ext4 (read-write) | erofs (read-only) |
| dm-verity | Not enabled | Enabled 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 runtime | Yes | No (I/O error then kernel panic) |
| Kernel modules | All modules including unsigned third-party drivers | Signed modules only (module.sig_enforce=1) |
| Kernel lockdown | Not enforced | lockdown=integrity (no unsigned code in ring 0) |
| Attack surface | Large: writable FS, package managers, update services | Minimal: 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:
- 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.
- Without dm-verity, nothing prevents a compromised process from modifying binaries on disk.
- Thousands of packages means a large attack surface.
- 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
| Component | Details |
|---|---|
| Guest OS | Ubuntu 24.04 LTS (Noble Numbat) |
| Kernel | Custom-built, ABI-pinned Ubuntu 6.17 kernel (CVM Guard hardening patches, TDX guest support), built and signed in CI |
| Root filesystem | erofs (read-only) |
| Integrity | dm-verity hash tree |
| Boot | shim then GRUB then kernel + initrd, each stage measured into the RTMRs; dm-verity root hash in cmdline |
| Boot integrity | Measured boot: every boot component recorded in the TDX RTMRs and checked via remote attestation (see above) |
| Partitions | ESP (512 MB) + root erofs (~940 MB) + verity hash (~63 MB) |
| Networking | systemd-networkd with DHCP |
| SSH | None: 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 support | tpm2-tools, clevis, cryptsetup |
| Cloud integration | google-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:
| Artifact | Description |
|---|---|
| dm-verity root hash | The 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 image | Bootable 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 MR | RTMR | TCG PCRs | What it measures |
|---|---|---|---|
| 1 | RTMR[0] | PCR 0 | Firmware code (TDVF/OVMF) |
| 2 | RTMR[1] | PCR 1–7 | EFI boot path: shim binary, GRUB binary, Secure Boot variables, EFI actions |
| 3 | RTMR[2] | PCR 8–15 | OS boot: GRUB commands, kernel binary, initrd, kernel command line (including roothash=), MOK list |
| 4 | RTMR[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_measurementsFinding 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:
- Edit the configuration in the cvm-images repository (add or update packages, bump the image version).
- Build the image with
sudo mkosi build. - Test locally with QEMU.
- Upload and register the new image on the target cloud platform.
- 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:
| Production | Dev | |
|---|---|---|
| SSH | No sshd binary on disk | openssh-server (key-based, via OS Login) |
| Debug tools | None | strace, 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
| Decision | Rationale |
|---|---|
| erofs over ext4 | Read-only by design. Smaller than ext4, ideal for dm-verity. No accidental writes possible. |
| Measured boot over UEFI Secure Boot | Stock 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-boot | GRUB 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 set | Every additional package increases the attack surface. ~40 packages means fewer potential CVEs than ~2000. |
| Signed kernel modules only | module.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 split | Debugging needs SSH; production must not have it. Two profiles of the same image, distinguishable in the attestation, instead of one compromise image. |
Containers & Workload Manifest
How Enclave OS Virtual dynamically loads OCI containers, the workload manifest format, and per-container attestation.
Disk Encryption
LUKS2+AEAD encryption for the OS data partition and per-container volumes in Enclave OS Virtual, with BYOK and Enclave Vaults key distribution.