Production-Grade KVM Virtualization on Ubuntu 22.04 LTS and Above
Turn Ubuntu 22.04 LTS+ into a production-grade KVM hypervisor with host bridging, IOMMU/VFIO hardware passthrough, and dedicated storage pools.
5 min read
On this page
Default hypervisor setups usually fall apart the moment you need real performance: VMs get stuck behind double-NAT, disks choke on default root partitions, Windows 11 refuses to install without TPM emulation, and GPU passthrough crashes because PCIe ACS grouping is a mess.
This guide turns a fresh Ubuntu 22.04 LTS or 24.04 LTS host into a rock-solid, production-grade KVM/QEMU hypervisor with routed bridge networking, dedicated storage pools, non-root libvirt socket management, and PCI/IOMMU prep for hardware passthrough.
1. Core Packages and Tooling
Install the hypervisor kernel modules, management daemons, UEFI firmware, virtual TPM engine, and guest filesystem utilities in a single pass:
sudo apt update && sudo apt -y install \
qemu-utils \
qemu-kvm \
virt-manager \
libvirt-daemon \
libvirt-daemon-system \
virtinst \
bridge-utils \
libvirt-clients \
ovmf \
swtpm \
virt-viewer \
tuned \
guestfs-tools \
libosinfo-bin
Component Breakdown
- qemu-kvm & libvirt-daemon-system: Hardware-assisted kernel hypervisor and background management services.
- ovmf & swtpm: UEFI firmware binaries and software TPM 2.0 emulation, mandatory for modern Windows guests and Secure Boot testing.
- guestfs-tools: Lets you modify, rescue, and inspect VM disk images directly from the host CLI without booting the guest.
- tuned: System tuning daemon to switch host CPU and I/O profiles to virtual-host mode.
2. Daemon Configuration and Non-Root Access
Running sudo virsh or launching virt-manager as root creates messy permission conflicts across disk images. Configure the libvirt UNIX socket so group members can manage VMs directly.
Configure Libvirt Socket Permissions
Edit /etc/libvirt/libvirtd.conf:
sudo nano /etc/libvirt/libvirtd.conf
Uncomment and set these socket permission directives:
unix_sock_group = "libvirt"
unix_sock_rw_perms = "0770"
Append structured logging at the bottom of the file to capture hypervisor faults cleanly:
log_filters="3:qemu 1:libvirt"
log_outputs="2:file:/var/log/libvirt/libvirtd.log"
Grant User Access
Add your host user to both the hardware acceleration (kvm) and management (libvirt) groups:
sudo usermod -aG kvm,libvirt $USER
Configure QEMU Execution Identity
If your VM disk images live inside your home directory, QEMU will throw Permission Denied errors under its default libvirt-qemu service account. Adjust /etc/libvirt/qemu.conf:
sudo nano /etc/libvirt/qemu.conf
Set the execution user and group to your host username:
user = "your_username"
group = "your_username"
Restart the daemon to apply configuration changes:
sudo systemctl restart libvirtd
3. Kernel Configuration: IOMMU, ACS Overrides, and VFIO
To enable PCI passthrough (discrete GPUs, NICs, or Intel GVT-g vGPU slicing), the kernel needs IOMMU enabled at boot with relaxed ACS isolation rules.
Identify Target PCI Devices
Identify your onboard or discrete graphics hardware addresses:
lspci -nnk | grep -A 3 VGA | grep -E "VGA|driver"
Update GRUB Boot Parameters
Edit /etc/default/grub:
sudo nano /etc/default/grub
Locate GRUB_CMDLINE_LINUX_DEFAULT and update it:
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash iommu=pt intel_iommu=on i915.enable_gvt=1 kvm.ignore_msrs=1 pcie_acs_override=downstream,multifunction vfio_iommu_type1.allow_unsafe_interrupts=1"
(On AMD systems, replace intel_iommu=on with amd_iommu=on and drop i915.enable_gvt=1.)
Parameter Breakdown
- intel_iommu=on / iommu=pt: Activates hardware IOMMU and sets pass-through mode for host devices, preventing unnecessary DMA translation overhead.
- i915.enable_gvt=1: Enables Intel Graphics Virtualization Technology (GVT-g) for splitting integrated GPUs into virtual instances.
- kvm.ignore_msrs=1: Silently ignores unhandled Model Specific Register calls from guests, preventing guest BSODs or panics.
- pcie_acs_override=downstream,multifunction: Splinters shared IOMMU groups so individual devices can be isolated cleanly.
- vfio_iommu_type1.allow_unsafe_interrupts=1: Allows VFIO operation on boards lacking full interrupt remapping hardware.
Rebuild the GRUB configuration:
sudo update-grub
Preload VFIO Kernel Modules
Create /etc/modules-load.d/pci-passthrough.conf to ensure VFIO drivers claim passthrough devices early in the boot sequence:
sudo tee /etc/modules-load.d/pci-passthrough.conf << 'EOF'
# Modules required for PCI passthrough
vfio
vfio_iommu_type1
vfio_pci
vfio_virqfd
# Module required for Intel GVT-g Split
kvmgt
EOF
Rebuild initial RAM filesystems across all installed kernels:
sudo update-initramfs -u -k all
4. Host Bridged Networking
Libvirt defaults to a NAT virtual network (virbr0), which isolates VMs behind host translation. For true lab work, VMs should live directly on your physical subnet with their own IP addresses assigned by your upstream router.
Configure Bridge with NetworkManager (nmcli)
Identify your primary physical interface (e.g., enp0s31f6) using ip link, then assemble the bridge:
# 1. Create the bridge master interface
sudo nmcli conn add type bridge con-name bridge0 ifname br0
# 2. Bind your physical NIC to the bridge
sudo nmcli conn add type ethernet slave-type bridge con-name bridge-br0 ifname enp0s31f6 master br0
# 3. Bring up the bridge slave and disable standalone physical connection
sudo nmcli conn up bridge-br0
sudo nmcli conn down "Wired connection 1"
Prevent Netfilter from Filtering Bridge Traffic
Linux bridges by default invoke iptables/nftables packet filtering, which can silently drop bridged VM traffic. Disable bridge netfilter processing:
sudo tee /etc/sysctl.d/bridge.conf << 'EOF'
net.bridge.bridge-nf-call-ip6tables=0
net.bridge.bridge-nf-call-iptables=0
net.bridge.bridge-nf-call-arptables=0
EOF
Add a udev rule to apply these sysctl flags automatically whenever the br_netfilter module gets loaded:
sudo tee /etc/udev/rules.d/99-bridge.rules << 'EOF'
ACTION=="add", SUBSYSTEM=="module", KERNEL=="br_netfilter", RUN+="/sbin/sysctl -p /etc/sysctl.d/bridge.conf"
EOF
Register the Bridge in Libvirt
Create an XML definition for libvirt to recognise br0:
cat << 'EOF' > /tmp/bridge.xml
<network>
<name>bridge</name>
<forward mode="bridge"/>
<bridge name="br0"/>
</network>
EOF
Import and activate the network in libvirt:
virsh net-define /tmp/bridge.xml
virsh net-start bridge
virsh net-autostart bridge
virsh net-autostart default
rm /tmp/bridge.xml
5. Dedicated Storage Pool Migration
Default VM disks land under /var/lib/libvirt/images, which often lives on a constrained root partition. Migrate the default pool to your fast, dedicated workspace storage.
# Teardown the default directory pool
sudo virsh pool-destroy default
sudo virsh pool-undefine default
# Create dedicated target directory
mkdir -p /home/michael/Workspace/machines/
chmod 755 /home/michael/Workspace/machines/
# Define and start the new storage pool
sudo virsh pool-define-as --name default --type dir --target /home/michael/Workspace/machines/
sudo virsh pool-autostart default
sudo virsh pool-start default
Verify storage and network status:
virsh pool-list --all
virsh net-list --all
6. Verification and Smoke Test
Restart the host to apply GRUB boot parameters and module preloads.
After restarting, verify the setup:
- Check IOMMU initialization:
dmesg | grep -E "DMAR|IOMMU" - Verify virtualization hardware support:
kvm-ok - Inspect bridge assignment:
ip addr show br0