简体中文 / [English]


Running Confidential Virtual Machines with AMD SEV-SNP on an Untrusted EPYC 9654

 

This article is currently an experimental machine translation and may contain errors. If anything is unclear, please refer to the original Chinese version. I am continuously working to improve the translation.

Background

I recently got access to a powerful EPYC 9654 server, on which I happily ran various data analysis tasks—even LLMs. But recently, when I wanted to process some private data on it, I hit a wall…

I’m not the only user on this machine. The permission setup is a textbook example of chaotic shared access—everyone shares the same account password, all users are in the sudo group, the root password circulates freely across WeChat/QQ groups, and the server’s SSH is exposed to the public internet via internal network tunneling. Even the IPMI shares the same weak username and password with the OS.

This environment is about as “secure” as a cardboard box. Any user or external attacker can easily read my data from disk or memory. And yet, such servers are widespread inside university intranets—everyone just wants convenience, no one cares about security. If I harden the server, it’ll only inconvenience others.


So I started exploring how to securely compute my data in this chaotic environment. After a quick search, I discovered AMD Secure Encrypted Virtualization (SEV).

Oh, this is confidential computing! Major cloud providers like Azure, AWS, and GCP all support it. It seems like a reasonably mature technology—shouldn’t be too hard to set up on my own server, right? (flag planted)

While trusted computing has a bad reputation in consumer markets—often used for vendor lock-in or DRM, enabling anti-user features that restrict freedom and choice by monopolizing the definition of “security” and “trust” for pure profit… (I’m looking at you, Google and Play Integrity)

At least on mainstream desktop x86 platforms, Secure Boot remains opt-outable, and memory encryption is still a privilege of server CPUs. Aside from needing to trust and rely on vendor-specific features like SEV, it’s overall a solid security addition. Let’s give it a try.

AMD’s SEV comes in several flavors:

  • SEV: Secure Encrypted Virtualization. Encrypts VM memory.
  • SEV-ES: Secure Encrypted Virtualization - Encrypted State. Encrypts VM memory and CPU register state.
  • SEV-SNP: Secure Encrypted Virtualization - Secure Nested Paging. Adds memory mapping encryption and integrity on top of the previous two.

SEV and SEV-ES require 2nd-gen EPYC or newer; SEV-SNP requires 3rd-gen EPYC or newer. Since the EPYC 9004 series supports it, we’ll go straight for the latest: SEV-SNP.

Setting Up the Environment

Documentation on SEV-SNP across the internet is sparse. I only found one detailed guide for SEV-ES (I tried using the parameters from that post and the QEMU Wiki, but failed to boot). So I’m documenting the SEV-SNP setup process here for reference.

Host BIOS

First, we need to modify the host BIOS. My server uses an H13SSL-N motherboard, and SEV-related settings are disabled by default. Simply reboot, connect via IPMI, and change the following in the web-based BIOS interface:

Enable SMEE (memory encryption), enable SEV Control, set SEV-ES ASID Space Limit to a value > 1 to enable SEV-ES, and enable RMP Table.

BIOS CPU Settings PageBIOS CPU Settings Page

Enable SEV-SNP Support

BIOS Southbridge Settings PageBIOS Southbridge Settings Page

Host Linux Configuration

With CPU and BIOS support in place, we now need support from the hypervisor (KVM), QEMU, and the OVMF firmware used by QEMU.

SEV-SNP is a relatively new technology. Only a few days ago did the latest Ubuntu 25.04 add host-side support. My Ubuntu 22.04 LTS is clearly out of luck for such updates, so I have to compile the required tools myself.

Luckily, there’s a kind soul on GitHub who’s already compiled the necessary scripts. We can just pull and use them. (Ideally, you should build these tools in a trusted environment—especially the OVMF firmware below—but I’m cutting corners here.)

Ubuntu 22.04 had some missing dependencies during build, like nasm, iasl, debhelper, etc. Just install them manually based on error messages.

1
2
3
4
5
6
7
8
9
git clone https://github.com/AMDESE/AMDSEV.git
git checkout snp-latest
# Build QEMU
./build.sh qemu
# Build OVMF (UEFI firmware) for QEMU
./build.sh ovmf
# Build kernel with SEV-SNP support
./build.sh kernel
sudo cp kvm.conf /etc/modprobe.d/

After successful build, install the host kernel via apt and reboot, selecting the new kernel in GRUB.

1
2
3
cd linux
apt install ./linux-headers-6.11.0-rc3-snp-host-85ef1ac03941_6.11.0-rc3-g85ef1ac03941-2_amd64.deb ./linux-libc-dev_6.11.0-rc3-g85ef1ac03941-2_amd64.deb ./linux-image-6.11.0-rc3-snp-host-85ef1ac03941_6.11.0-rc3-g85ef1ac03941-2_amd64.deb
reboot

After the host reboots, you should see SEV-SNP loading logs in dmesg.

1
2
3
4
5
6
7
8
9
10
11
12
13
test@epyc:~$ uname -a
Linux epyc 6.11.0-rc3-snp-host-85ef1ac03941 #2 SMP Sun Aug 24 02:32:26 CST 2025 x86_64 x86_64 x86_64 GNU/Linux
test@epyc:~$ sudo dmesg | grep SEV
[ 0.000000] SEV-SNP: RMP table physical range [0x0000000015500000 - 0x0000000075afffff]
[ 0.003519] SEV-SNP: Reserving start/end of RMP table on a 2MB boundary [0x0000000015400000]
[ 0.003526] SEV-SNP: Reserving start/end of RMP table on a 2MB boundary [0x0000000075a00000]
[ 13.101084] ccp 0000:03:00.5: SEV API:1.55 build:36
[ 13.101094] ccp 0000:03:00.5: SEV-SNP API:1.55 build:36
[ 13.127843] kvm_amd: SEV enabled (ASIDs 128 - 1006)
[ 13.127844] kvm_amd: SEV-ES enabled (ASIDs 1 - 127)
[ 13.127845] kvm_amd: SEV-SNP enabled (ASIDs 1 - 127)
test@epyc:~$ cat /sys/module/kvm_amd/parameters/sev_snp
Y

Launching the Guest

Prepare a Linux guest disk image in qcow2 format and transfer it to the host. I chose Debian 13 here—newer distros already include SEV-SNP guest support.

1
2
3
# Launch the VM
# P.S. I edited the script to add -netdev user,id=vmnic,hostfwd=tcp::8000-:22 -device e1000,netdev=vmnic,romfile= -vnc :1 -device virtio-vga, enabling port forwarding and VNC for easier monitoring
sudo ./launch-qemu.sh -hda ../debian13-secure.qcow2 -sev-snp -mem 16384 -smp 16

Connect to the running VM via SSH or other methods. In dmesg, you’ll see SEV-SNP has started in the guest.

1
2
3
4
5
6
7
8
root@sevsnp:~# dmesg | grep SEV
[ 2.350837] Memory Encryption Features active: AMD SEV SEV-ES SEV-SNP
[ 2.350855] SEV: Status: SEV SEV-ES SEV-SNP
[ 2.470226] SEV: APIC: wakeup_secondary_cpu() replaced with wakeup_cpu_via_vmgexit()
[ 3.806622] SEV: Using SNP CPUID table, 29 entries present.
[ 3.806628] SEV: SNP running at VMPL0.
[ 4.118602] SEV: SNP guest platform device initialized.
[ 15.268999] sev-guest sev-guest: Initialized SEV guest driver (using VMPCK0 communication key)

Successfully launched SEV-SNP protected guest VMSuccessfully launched SEV-SNP protected guest VM

Performing Guest Attestation

It looks like we’ve successfully launched an SEV-SNP protected VM. The VM’s memory and register state are now protected by the AMD Secure Processor and cannot be read or tampered with by the untrusted hypervisor (in this case, our EPYC 9654 server).

But consider this scenario: suppose a sophisticated attacker, when I transferred the Debian13 qcow2 image to the EPYC 9654, immediately tampered with and replaced the kernel so that it fakes SEV-SNP protection—printing SEV-SNP logs even though no protection is active—and I end up believing it’s secure.

In other words, we currently can’t prove that the VM is actually protected. To further confirm that the VM is under SEV-SNP protection and running trusted software, we need to perform remote Attestation.

Preparing the measurement

Let’s move to a trusted device—like my HomeLab—for preparation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# Install tools for generating measurement
pip install sev-snp-measure

# Rebuild OVMF in a trusted environment
git clone https://github.com/AMDESE/AMDSEV.git
cd AMDSEV
git checkout snp-latest
# Make a small change before building: replace OvmfPkg/OvmfPkgX64.dsc with OvmfPkg/AmdSev/AmdSevX64.dsc in common.sh
# See https://github.com/virtee/sev-snp-measure/issues/26#issuecomment-1636995518
touch ovmf/OvmfPkg/AmdSev/Grub/grub.efi # This touch comes from https://github.com/AMDESE/AMDSEV/issues/124#issuecomment-1336387666
./build.sh ovmf
cd ..
cp AMDSEV/ovmf/Build/AmdSev/DEBUG_GCC5/FV/OVMF.fd .

# For convenience, just "borrow" kernel and initramfs from current system (Ubuntu 22.04, which already supports SNP as guest)
sudo cp /boot/vmlinuz .
# Need to pack the required sev-guest driver into initramfs
echo -e '\nsev-guest' > /etc/initramfs-tools/modules
sudo update-initramfs -u
sudo cp /boot/initrd.img .

# Also pack in the snpguest tool we need
sudo unmkinitramfs -v initrd.img .
wget https://github.com/virtee/snpguest/releases/download/v0.9.1/snpguest
sudo chown root:root snpguest
sudo chmod 755 snpguest
sudo cp snpguest main/usr/bin
sudo cp /usr/bin/base64 main/usr/bin
cd main
find . | sudo cpio -o -H newc | gzip > ../myinitrd.cpio.gz
cd ..

# Generate the measurement value—this is what we'll verify later
sev-snp-measure --mode snp --vcpus 16 --vcpu-type EPYC-v4 --ovmf OVMF.fd --kernel vmlinuz --initrd myinitrd.cpio.gz --append "console=ttyS0"
# Output: 4936d42a8e2c2e91f1f1160063097b43b5c0c3339f5f85586128356e16d4cf36ebe5657fbbf9c3163dcf606bdf7e542c

Launching the Guest

Copy the generated OVMF.fd, vmlinuz, and myinitrd.cpio.gz files to the EPYC 9654.

1
2
3
# Need to edit launch-qemu.sh
# Append ,kernel-hashes=on to the -object sev-snp-guest,...,reduced-phys-bits=1 line
sudo ./launch-qemu.sh -sev-snp -bios .. -cpu EPYC-v4 -smp 16 -mem 16384 -kernel ../vmlinuz -initrd ../myinitrd.cpio.gz -append "console=ttyS0" -hda ""

From the logs, the actual executed command is:

1
/mnt/ssd/qemu/AMDSEV/usr/local/bin/qemu-system-x86_64 -enable-kvm -cpu EPYC-v4 -machine q35 -netdev user,id=vmnic,hostfwd=tcp::8000-:22 -device virtio-net-pci,disable-legacy=on,iommu_platform=true,netdev=vmnic,romfile= -vnc :1 -device virtio-vga -smp 16,maxcpus=255 -m 16384M,slots=5,maxmem=24576M -no-reboot -bios /mnt/ssd/qemu/OVMF.fd -machine confidential-guest-support=sev0,vmport=off -object memory-backend-memfd,id=ram1,size=16384M,share=true,prealloc=false -machine memory-backend=ram1 -object sev-snp-guest,id=sev0,policy=0x30000,cbitpos=51,reduced-phys-bits=1,kernel-hashes=on -kernel ../vmlinuz -append "console=ttyS0" -initrd ../myinitrd.cpio.gz -nographic -monitor pty -monitor unix:monitor,server,nowait

After successful boot, since no disk is mounted, initramfs fails to proceed and stops at the busybox shell.

VM booted into initramfsVM booted into initramfs

Performing Attestation and Verifying Results

Back on the trusted machine, generate a nonce (a common practice in modern security to prevent replay attacks):

1
2
openssl rand -hex 64 > nonce.hex
# This results in: 3502ae46269024fda5eea969d942f15b9cf9708602709d97b0de1ceaa0b712c7a6ea5170310fd6f10e9f1ad223cb4ffbc8fd036a70846cb7d50f3086c64c2da0

On the VM, write the nonce to a file and generate an attestation report:

1
2
3
4
5
6
echo '3502ae46269024fda5eea969d942f15b9cf9708602709d97b0de1ceaa0b712c7a6ea5170310fd6f10e9f1ad223cb4ffbc8fd036a70846cb7d50f3086c64c2da0' > nonce.hex
# Use the previously prepared snpguest tool to generate report.bin
snpguest report report.bin nonce.hex
# Read the file back
cat report.bin | base64
# Output: AgAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAJ...<truncated>

Transfer report.bin back to the trusted HomeLab machine and verify:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Download AMD's CA certificates for Genoa (EPYC 9004 series) (ark.pem + ask.pem)
snpguest fetch ca pem . genoa

# Fetch vcek.pem from AMD KDS service using the attestation report
# VCEK (Versioned Chip Endorsement Key) is unique per CPU
snpguest fetch vcek -p genoa pem . report.bin

# We now have the trust chain: ARK (AMD Root Key) -> ASK (AMD SEV Key) -> VCEK
snpguest verify certs .
# Output:
# The AMD ARK was self-signed!
# The AMD ASK was signed by the AMD ARK!
# The VCEK was signed by the AMD ASK!

# Verify the attestation report
snpguest verify attestation -p genoa . report.bin
# Output:
# Reported TCB Boot Loader from certificate matches the attestation report.
# Reported TCB TEE from certificate matches the attestation report.
# Reported TCB SNP from certificate matches the attestation report.
# Reported TCB Microcode from certificate matches the attestation report.
# VEK signed the Attestation Report!

We now have a cryptographically verified attestation report signed by AMD. Use snpguest display report report.bin to inspect its content.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
Attestation Report:

Version: 2

Guest SVN: 0

Guest Policy (0x30000):
ABI Major: 0
ABI Minor: 0
SMT Allowed: true
Migrate MA: false
Debug Allowed: false
Single Socket: false

Family ID:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Image ID:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

VMPL: 1

Signature Algorithm: 1

Current TCB:

TCB Version:
Microcode: 72
SNP: 21
TEE: 0
Boot Loader: 9
FMC: None

Platform Info (1):
SMT Enabled: true
TSME Enabled: false
ECC Enabled: false
RAPL Disabled: false
Ciphertext Hiding Enabled: false
Alias Check Complete: false

Key Information:
author key enabled: false
mask chip key: false
signing key: vcek

Report Data:
33 35 30 32 61 65 34 36 32 36 39 30 32 34 66 64
61 35 65 65 61 39 36 39 64 39 34 32 66 31 35 62
39 63 66 39 37 30 38 36 30 32 37 30 39 64 39 37
62 30 64 65 31 63 65 61 61 30 62 37 31 32 63 37

Measurement:
49 36 D4 2A 8E 2C 2E 91 F1 F1 16 00 63 09 7B 43
B5 C0 C3 33 9F 5F 85 58 61 28 35 6E 16 D4 CF 36
EB E5 65 7F BB F9 C3 16 3D CF 60 6B DF 7E 54 2C

Host Data:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

ID Key Digest:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Author Key Digest:
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Report ID:
C5 13 43 3F A5 F7 D2 F9 24 91 AE AE DD B2 33 D3
5A 23 4A 81 90 90 F4 CA A6 75 8B 38 A0 F3 05 8C

Report ID Migration Agent:
FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF
FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF

Reported TCB:

TCB Version:
Microcode: 72
SNP: 21
TEE: 0
Boot Loader: 9
FMC: None

CPUID Family ID: None

CPUID Model ID: None

CPUID Stepping: None

Chip ID:
59 54 5A 6F D7 AF 46 35 51 28 B9 55 4B 1A 0C 86
6D 88 1A A5 E6 5F CB 02 24 49 51 78 F1 0B 57 7D
C4 1D FC 64 82 57 E1 AC DB DB 2E 03 24 17 F2 02
A3 F1 D3 48 60 38 52 A3 10 62 7C 72 AA 27 68 BD

Committed TCB:

TCB Version:
Microcode: 72
SNP: 21
TEE: 0
Boot Loader: 9
FMC: None

Current Version: 1.55.36

Committed Version: 1.55.36

Launch TCB:

TCB Version:
Microcode: 72
SNP: 21
TEE: 0
Boot Loader: 9
FMC: None

Signature:
R:
23 48 89 73 E1 AE D6 B1 31 6C 8A 79 00 DF DA DB
84 CB C6 0A 1D EC AE 71 0D 7C E1 7A 33 C1 80 C1
86 1A A4 05 08 A0 A4 A9 C9 AD 5B 1B D8 03 A6 C2
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
S:
D8 AC 18 0F 3F 47 E8 F2 6A 8E AC 6C F2 A8 04 E3
1B 41 C0 4E F1 5A 51 30 79 8E C0 A0 69 61 73 EE
B2 FC 6B 6C 72 84 F3 1B 0E 3A 22 B1 9B AB 2B D1
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00

You can see our generated nonce in the Report Data field, and the Measurement value matches the one we computed locally.

Now, as long as we trust AMD, we have cryptographically confirmed that: this VM is indeed running on a real, trusted AMD system; its memory confidentiality and integrity are protected by AMD SEV-SNP; and it’s running the exact software image (OVMF, kernel, initramfs, cmdline) we prepared in a trusted environment, unaltered.

Injecting Keys and Completing Boot

After attestation, we now have good reason to believe we’ve successfully launched a trusted VM on the shady EPYC 9654 server. The hardest part is done. Now it should be straightforward to boot a full Linux distro and start computing… right?

Think again.

Let’s reconsider: we’ve only booted a specific kernel and initramfs. To run a complete Linux system, we still need a trusted rootfs. Even if you’re bold enough to pack the entire environment into initramfs, our ultimate goal is to bring in confidential data into the VM’s memory for computation.

Packing secrets, keys, or private data directly into initramfs won’t work—it’s plaintext and not encrypted. The same goes for any credentials. Transmitting keys or secrets over network or terminal is also unsafe, as the communication can be easily intercepted by the hypervisor (the middleman). You might think of SSH, but SSH security relies on the host key not being compromised—except the hypervisor already has access to the SSH host key stored in initramfs.

The core issue is this: the hypervisor currently has the same information as our encrypted VM. Even though we know such a VM exists, we cannot distinguish between the VM and the hypervisor when communication is involved.


In older SEV-ES, we could generate encrypted secrets that AMD’s CPU would decrypt and pass directly to the VM—simple and effective. But in the newer SEV-SNP, this seems replaced by a much more complex SVSM module. (See related discussion)

This part stumped me for a while. Guess my modern cryptography knowledge isn’t quite up to par. But actually, by cleverly using the attestation process we just completed, we can now distinguish the encrypted VM from the hypervisor.

The idea is:

  1. In a trusted environment, generate a nonce.
  2. Provide the nonce to the encrypted VM; the VM uses it as Report Data to generate an attestation report.
  3. The encrypted VM generates a key pair in memory.
  4. The VM uses the public key as Report Data to generate a second attestation report.
  5. In the trusted environment, verify both reports. The first proves the VM is trustworthy; the second carries a public key signed by AMD, which the malicious hypervisor cannot tamper with.
  6. Encrypt your secret (e.g., disk encryption key) using this public key.
  7. The VM uses its private key to decrypt the message and obtain the secret in memory.

Note: The initramfs must not trust terminal input—no shell access should be exposed. It should only accept data through this secure flow. Since the attestation report includes the initramfs hash, we can fully control its behavior by carefully writing the init script.

This process is relatively complete. As a shortcut, you could just generate and verify the second report—effectively using a nonce generated inside the VM.

Theory is sound, time to implement

Back on the trusted HomeLab, prepare an encrypted rootfs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
truncate -s 5G rootfs_enc.raw

sudo losetup -f rootfs_enc.raw
losetup -a # My stubborn snap assigned /dev/loop28—will use that

# Encrypt the entire virtual disk with LUKS. Set password here, e.g., "AMD,YES!"
# Default LUKS lacks data integrity: https://security.stackexchange.com/questions/87367/does-luks-protect-the-filesystem-integrity
# So we use LUKS2 and manually enable integrity (still experimental)
sudo cryptsetup luksFormat --type luks2 /dev/loop28 --integrity hmac-sha256
sudo cryptsetup luksOpen /dev/loop28 guest_rootfs
sudo mkfs.ext4 /dev/mapper/guest_rootfs

mkdir rootfs
sudo mount /dev/mapper/guest_rootfs rootfs

sudo debootstrap --arch=amd64 jammy rootfs http://mirror.nju.edu.cn/ubuntu/

sudo chroot rootfs # chroot into rootfs for setup
passwd root # Set login password, or create a new user
# Configure network for SSH (terminal password login is unsafe)
cat << EOF > /etc/netplan/01-config.yaml
network:
version: 2
ethernets:
enp0s1:
dhcp4: true
EOF

apt update && apt install ssh # Also note SSH keys; if only root, add pubkey or allow password login
exit # exit chroot

sudo umount rootfs
sudo cryptsetup luksClose guest_rootfs
sudo losetup -d /dev/loop28

Now prepare the initramfs with the “handshake” logic. The built-in /init script is too complex for me to fully understand, so I’ll replace it entirely—functionality remains intact.

Also, if you just installed cryptsetup while creating the rootfs, your unpacked initrd.img might not include it. Repack it. You may also need to add dm_integrity to /etc/initramfs-tools/modules and re-update and unpack initrd.img.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
#!/bin/sh
# This script completely replaces the original init in initramfs
# I'm using 'age' for encryption/decryption—compiled with CGO_ENABLED=0, so no dynamic linking issues. Use any tool you prefer.
# Current implementation is interactive. With network setup, you could automate this securely.

LUKS_DEVICE="/dev/sda"
MAPPER_NAME="encroot"
FS_TYPE="ext4"
NEW_ROOT_MP="/new_root"

echo "==> [INITRAMFS] Starting LUKS init script..."
echo "==> [INITRAMFS] Mounting essential filesystems..."
mkdir /proc /sys
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev

echo "==> [INITRAMFS] Loading crypto modules..."
modprobe sev-guest
modprobe dm_crypt
modprobe dm_integrity

echo "==> [INITRAMFS] Starting remote attestation process..."

read -r -p "Enter nonce:" NONCE

echo " -> Generating attestation report with nonce..."
echo -n $NONCE > /nonce.txt
snpguest report /report1.bin /nonce.txt

if [ $? -ne 0 ]; then
echo "!!! [INITRAMFS] FAILED to get attestation report."
exit 1
fi

echo " -> Generating ephemeral key pair..."
age-keygen -o /vm_key.txt
EPHEMERAL_PUBLIC_KEY=$(grep "public key: " /vm_key.txt | cut -d ' ' -f 4)
echo " -> Public key generated."

echo " -> Generating attestation report with public key..."
echo -n "$EPHEMERAL_PUBLIC_KEY " > /pub.txt # age pubkey is 62 bytes; padding to 64 bytes. If you can pass raw key, skip hashing
snpguest report /report2.bin /pub.txt

if [ $? -ne 0 ]; then
echo "!!! [INITRAMFS] FAILED to get attestation report."
exit 1
fi

echo " -> Attestation report generated."

echo "----- REPORT 1 -----"
cat /report1.bin | base64
echo "----- REPORT 2 -----"
cat /report2.bin | base64

read -r -p "Enter encrypted LUKS key (age format, one-line base64):" ENCRYPTED_LUKS_KEY_B64
echo $ENCRYPTED_LUKS_KEY_B64 | base64 -d > /luks.age

echo "==> [INITRAMFS] Decrypting LUKS key..."
DECRYPTED_LUKS_KEY=$(age --decrypt -i /vm_key.txt /luks.age)
if [ $? -ne 0 ] || [ -z "${DECRYPTED_LUKS_KEY}" ]; then
echo "!!! [INITRAMFS] FAILED to decrypt LUKS key."
exit 1
fi

rm /vm_key.txt /luks.age /pub.txt /report1.bin /report2.bin

echo "==> [INITRAMFS] Attempting to unlock LUKS device: ${LUKS_DEVICE}"
echo -n "${DECRYPTED_LUKS_KEY}" | cryptsetup luksOpen ${LUKS_DEVICE} ${MAPPER_NAME} --key-file -

if [ $? -ne 0 ]; then
echo "!!! [INITRAMFS] FAILED to unlock LUKS device with provided key."
exit 1
fi

echo "==> [INITRAMFS] LUKS device unlocked successfully as /dev/mapper/${MAPPER_NAME}"

echo "==> [INITRAMFS] Creating mount point for the real root..."
mkdir ${NEW_ROOT_MP}

echo "==> [INITRAMFS] Mounting decrypted root filesystem..."
mount -t ${FS_TYPE} /dev/mapper/${MAPPER_NAME} ${NEW_ROOT_MP}

if [ $? -ne 0 ]; then
echo "!!! [INITRAMFS] FAILED to mount decrypted root filesystem."
# exec /bin/sh # For debugging only
exit 1
fi

echo "==> [INITRAMFS] Real root filesystem mounted successfully."

echo "==> [INITRAMFS] Unmounting temp filesystems..."
umount /proc
umount /sys
umount /dev

echo "==> [INITRAMFS] Switching to real root and executing init..."
exec switch_root ${NEW_ROOT_MP} /sbin/init

echo "!!! [INITRAMFS] FAILED to switch_root."
exit 1

Pack this init script and all required tools into the initramfs. Compute the measurement value locally, then send everything to the server. Preparation is finally complete!

Start the VM, provide a random nonce locally, and you’ll get two report.bin files.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
[    7.791052] Run /init as init process
==> [INITRAMFS] Starting LUKS init script...
==> [INITRAMFS] Mounting essential filesystems...
==> [INITRAMFS] Loading crypto modules...
[ 7.829855] sev-guest sev-guest: Initialized SEV guest driver (using vmpck_id 0)
[ 7.850841] xor: automatically using best checksumming function avx
[ 7.857134] async_tx: api initialized (async)
==> [INITRAMFS] Starting remote attestation process...
Enter nonce:f5c837b576373887112d46fcd20a77ac14ca5be663ca6d4913bd79e13e8d7c2e
-> Generating attestation report with nonce...
-> Generating ephemeral key pair...
Public key: age15lz45488nzmk3q4kmfdg9489s7652h0h04vdekjhnptwa5wnff3snv45ly
-> Public key generated.
-> Generating attestation report with public key...
-> Attestation report generated.
----- REPORT 1 -----
AgAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAJ
<truncated...>
----- REPORT 2 -----
AgAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAEAAAAJ
<truncated...>
Enter encrypted LUKS key (age format, one-line base64):

Locally, verify both reports’ signatures and confirm both measurement values match our local computation.

Then, verify the first report’s Report Data matches our random nonce, extract the public key from the second report’s Report Data, encrypt our LUKS key, and pass it to the VM.

1
2
3
4
5
6
7
8
9
snpguest verify attestation -p genoa . report1.bin
# VEK signed the Attestation Report!
snpguest verify attestation -p genoa . report2.bin
# VEK signed the Attestation Report!

snpguest display report report1.bin # Verify Measurement and Nonce
snpguest display report report2.bin # Verify Measurement, extract age public key

echo -n "AMD,YES\!" | age -r age15lz45488nzmk3q4kmfdg9489s7652h0h04vdekjhnptwa5wnff3snv45ly | base64 -w 0

Paste the resulting base64-encoded encrypted key into the VM. Watch as it decrypts successfully and the boot continues—we’re now in a familiar Ubuntu 22.04 system!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Enter encrypted LUKS key (age format, one-line base64):YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBITW5Bclp1ZmNiU01yZmlicmhpWmJaMG1KUExRTW5DNnI4Nmo3K3FZNUBBCkZwQU5EelNtNjN2SDlQeUtBVEVGZSs0dk5WWldMTUhLSTlCblY3c3k2bE0KLS0tIFhWR3IvY0NUSkZuemplS0FRa0ozbGRvQ0Y4WS81NDQrdGJQUkVyMXBiRWsKR3IVz1uPSJtD/Uc9ojYZ2fChdRumQk7YxAKv5JUbffWvFevi/Lhyww==
==> [INITRAMFS] Decrypting LUKS key...
==> [INITRAMFS] Attempting to unlock LUKS device: /dev/sda
==> [INITRAMFS] LUKS device unlocked successfully as /dev/mapper/encroot
==> [INITRAMFS] Creating mount point for the real root...
==> [INITRAMFS] Mounting decrypted root filesystem...
[493.983329] EXT4-fs (dm-1): mounted filesystem b155f911-e042-4949-b6bb-3a01f66445bf r/w with ordered data mode. Quota mode: none.
==> [INITRAMFS] Real root filesystem mounted successfully.
==> [INITRAMFS] Unmounting temp filesystems...
==> [INITRAMFS] Switching to real root and executing init...
[494.535955] systemd[1]: Failed to look up module alias 'autofs4': Function not implemented
[494.571956] systemd[1]: systemd 249.11-0ubuntu3 running in system mode (+PAM +AUDIT +SELINUX +APPARMOR +IMA +SMACK +SECCOMP +GCRYPT +GNUTLS -OPENSSL +ACL +BLKID +CURL +ELFUTILS -FIDO2 +IDN2 -IDN +IPTC +KMOD +LIBCRYPTSETUP -LIBFDISK +PCRE2 -PWQUALITY -P11KIT -QRENCODE +BZIP2 +LZ4 +XZ +ZLIB +ZSTD -XKBCOMMON +UTMP +SYSVINIT default-hierarchy=unified)
[494.578432] systemd[1]: Detected virtualization kvm.
[494.579548] systemd[1]: Detected architecture x86-64.

Welcome to Ubuntu 22.04 LTS!

<Then normal boot proceeds>

Soon after, the boot completes, and we see the login prompt. But don’t log in directly via QEMU console—it’s unsafe. We’ve already configured SSH and recorded the host key. Just connect securely via SSH.

A seemingly ordinary VM after successful bootA seemingly ordinary VM after successful boot

Afterword

This was my first time experimenting with trusted computing. Aside from manually building QEMU and the kernel, the overall process and tooling weren’t too complex. Compared to pure SEV/SEV-ES attestation, it’s much simpler now. I also got to revisit the Linux boot process and built a trust chain from UEFI firmware → kernel → initramfs → Ubuntu. I didn’t expect a minimal initramfs to be this straightforward when I started.

This blog came from a sudden idea—ultimately succeeding in building a secure computing environment on a machine where both hardware and software are fully untrusted. That’s pretty cool. Took me a few days, and who even remembers I just wanted to verify my restic backup on the server… Honestly, my files are so boring no one would care anyway.

This article is licensed under the CC BY-NC-SA 4.0 license.

Author: lyc8503, Article link: https://blog.lyc8503.net/en/post/amd-sev-snp/
If this article was helpful or interesting to you, consider buy me a coffee¬_¬
Feel free to comment in English below o/