Booting a UEFI Secure Boot Guest with libvirt on a VPS

Getting a UEFI guest to boot under libvirt on a cloud VPS is mostly an exercise in knowing which three files matter and where they live. A stock Debian or Ubuntu KVM host has OVMF_CODE.fd (read-only firmware, often shared) and an empty per-domain NVRAM copy. If the firmware can’t locate a bootable ESP, or the NVRAM copy is missing, you get “No bootable device” or a return to the UEFI shell. On a small VPS with 2 vCPU and 4 GB RAM that costs an afternoon, not a support ticket.

This walkthrough covers the QEMU/libvirt side: installing OVMF correctly, giving each guest its own writable vars file, attaching virtio drivers so the installer actually sees a disk, and passing the right nested-virtualization flags. It assumes you already have root on a KVM-enabled box, which you can confirm with ls /dev/kvm and egrep -c '(vmx|svm)' /proc/cpuinfo. If that check fails, no amount of libvirt configuration will help — choose a KVM-based server rather than an emulated one before you start.

Confirm the host can actually do nested KVM

Nested virtualization needs kvm_intel or kvm_amd with nested=1, plus a CPU model that exposes VMX/SVM to the guest. Check the module parameter and the CPU flags separately; a host can load the module while the hypervisor above it still masks the flag. When the flag is absent, libvirt silently falls back to TCG emulation and the guest runs at roughly one-twentieth the speed, which people usually misdiagnose as a disk problem.

cat /sys/module/kvm_intel/parameters/nested   # expect Y or 1
echo 'options kvm_intel nested=1' > /etc/modprobe.d/kvm-nested.conf
modprobe -r kvm_intel && modprobe kvm_intel
grep -m1 -oE 'vmx|svm' /proc/cpuinfo
virsh domcapabilities | grep -i -m3 'domain type\|machine'

Use a per-domain OVMF vars file, never the shared template

The classic mistake is pointing <nvram> at /usr/share/OVMF/OVMF_VARS.fd directly. Firmware writes to that file at boot, so two guests sharing it corrupt each other’s Secure Boot variables and boot order. Copy it once per domain into a private location and let libvirt own the copy. On Debian and Ubuntu the firmware lives under /usr/share/OVMF/; on RHEL-family hosts look in /usr/share/edk2/ovmf/.

cp /usr/share/OVMF/OVMF_VARS_4M.ms.fd /var/lib/libvirt/qemu/nvram/websrv_VARS.fd
virsh pool-refresh default
virsh define /etc/libvirt/qemu/websrv.xml

<os firmware="efi">
  <type arch="x86_64" machine="q35">hvm</type>
  <loader readonly="yes" secure="yes" type="pflash">
    /usr/share/OVMF/OVMF_CODE_4M.secboot.fd
  </loader>
  <nvram template="/usr/share/OVMF/OVMF_VARS_4M.ms.fd">
    /var/lib/libvirt/qemu/nvram/websrv_VARS.fd
  </nvram>
</os>

The secure="yes" attribute is what tells libvirt to keep the SMM and Secure Boot metadata consistent. Without it the guest boots in UEFI mode but the firmware reports Secure Boot disabled, and the installer’s shim fails verification. The 4M firmware variants are required for anything that needs SMM; the 2M ones work only for plain UEFI. Pick one family and stay consistent, because mixing 2M code with 4M vars produces a firmware error rather than a clean message.

Machine type and firmware must agree

Use q35, not i440fx. q35 gives you PCIe, an EHCI/UHCI-free layout and the ACPI tables OVMF expects for boot-option handling. Combined with firmware="efi", libvirt will generate the right device ordering. If you migrated an XML from a BIOS guest, the leftover <boot dev="hd"/> entries still apply but the order inside the ESP wins, which confuses people who assume the XML is authoritative.

SymptomUsual cause
Drops to UEFI shellNo ESP partition or missing bootx64.efi
“Security Violation”Unsigned kernel/shim, or secure=”yes” missing
Firmware error at start2M code paired with 4M vars
Installer sees no diskvirtio-blk without the driver in the ISO
Extremely slow bootNested KVM off, running under TCG

Getting virtio drivers into the installer

Windows installers have no virtio storage driver, and minimal Linux ISO variants sometimes lack virtio_blk. Two options work reliably: attach a secondary CD-ROM with the virtio-win ISO for Windows, or switch the disk bus to sata for the install and flip it to virtio afterwards. For Linux, use the full netinst image; the virtio modules are in-tree and load automatically once the device is present.

<disk type="file" device="disk">
  <driver name="qemu" type="qcow2" cache="none" io="native"/>
  <source file="/var/lib/libvirt/images/websrv.qcow2"/>
  <target dev="vda" bus="virtio"/>
  <boot order="1"/>
</disk>

Keep bus="virtio" on the network card too; emulated e1000 works but burns CPU on a 2-vCPU VPS where you can least afford it. Verify inside the guest with lsblk -o NAME,TRAN (expect virtio) and ethtool -i eth0 (driver should be virtio_net). If either shows emulated hardware, write throughput typically halves on small instances.

Passing nested flags through the CPU model

If the guest itself runs containers with hardware isolation or another hypervisor, it needs vmx visible. The clean way is <cpu mode="host-passthrough"/>, optionally with a feature policy. host-model usually works but can strip the flag on older libvirt releases. Do not add <feature policy="require" name="vmx"/> unless the host really exposes it; libvirt will refuse to start the domain rather than degrade gracefully.

<cpu mode="host-passthrough" check="none" migratable="off">
  <feature policy="require" name="vmx"/>
</cpu>

Verifying Secure Boot state instead of guessing

Inside the guest, mokutil --sb-state is the fastest answer on Linux. On a fresh install with shim and a signed kernel it should print SecureBoot enabled. If it says disabled, re-check that the loader line includes secure="yes" and that you did not accidentally point at the non-secboot firmware file. The two files differ by a few kilobytes of name and a completely different boot behaviour.

Track the NVRAM file’s mtime after a settings change; if it never updates, the guest is writing to a read-only template and your boot-order edits will vanish on reboot. A 4 KB change after efibootmgr -o is the confirmation you want.

Resource sizing for a nested guest

Nested guests pay a memory and CPU tax. Budget at least 2 GB for the host OS, 2 GB for the outer guest and 1:1 vCPU mapping with the host, and expect 10–25 percent overhead on I/O-heavy work. On a 4 GB VPS that leaves little room, which is why most people should run the workload directly rather than inside a nested hypervisor.

VPS RAMSafe outer guestNotes
2 GBNot viableHost alone uses ~700 MB
4 GB1 GB, 1 vCPUTest-only
8 GB4 GB, 2 vCPUComfortable
16 GB10 GB, 4 vCPURoom for a second guest

Once the guest boots, run virsh domstate --reason after a reboot cycle. A reason of “unknown” for a UEFI domain almost always means the NVRAM path is wrong or the file is not writable by the qemu user. Fix ownership with chown qemu:qemu and SELinux context with restorecon -Rv before assuming a firmware bug. Following these steps on a properly virtualized KVM instance — the kind provisioned through the server platform described here — gives you a reproducible Secure Boot guest in under an hour.

Leave a Reply