Guest OS Setup¶
On this page, we will introduce how an Intel TDX-enabled guest image can be generated and how a TD using this image can be started. We assume that the host OS setup was done before.
Prepare an Intel TDX-enabled Guest Image¶
To start an Intel TDX protected VM (i.e., a TD), you must create an Intel TDX-enabled guest OS image. Use the following instructions for your distribution of choice:
-
Install the packages required to create and launch an Intel TDX-enabled cloud image:
sudo dnf install -y guestfs-tools qemu-kvm libvirtsudo zypper install -y qemu qemu-tools guestfs-toolssudo dnf install -y guestfs-tools qemu-kvm libvirtsudo zypper install -y qemu qemu-tools guestfs-toolssudo apt install -y qemu-system-x86 cloud-image-utils libguestfs-tools \ qemu-utils libvirt-daemon-system libvirt-clients ovmf ovmf-inteltdx -
Add the current user to the appropriate groups:
sudo usermod -aG qgsd $USERThe
qgsdgroup grants access to the QGS's Unix socket for TD Quote generation. Add only trusted users to this group.sudo usermod -aG kvm,qgsd $USERThe
kvmgroup grants access to/dev/kvmfor QEMU hardware virtualization acceleration. Theqgsdgroup grants access to the QGS's Unix socket for TD Quote generation.sudo usermod -aG kvm,qgsd $USERThe
kvmgroup grants access to/dev/kvmfor QEMU hardware virtualization acceleration. Theqgsdgroup grants access to the QGS's Unix socket for TD Quote generation. Add only trusted users to these groups.sudo usermod -aG kvm,qgsd $USERThe
kvmgroup grants access to/dev/kvmfor QEMU hardware virtualization acceleration. Theqgsdgroup grants access to the QGS's Unix socket for TD Quote generation.sudo usermod -aG kvm,libvirt,qgsd $USERThe
kvmgroup grants access to/dev/kvmfor QEMU hardware virtualization acceleration. Theqgsdgroup grants access to the QGS's Unix socket for TD Quote generation. Thelibvirtgroup grants access to manage the system libvirt daemon, which can configure VMs with host resources and should be treated as root-equivalent. Add only trusted users to these groups.For the above new group additions to take effect, log out and back in again.
-
Create an SSH key pair to allow SSH access to the TD after it is launched:
ssh-keygen -t ed25519 -f ./guest_TD_root_id -N "" -qssh-keygen -t ed25519 -f ./guest_TD_root_id -N "" -qssh-keygen -t ed25519 -f ./guest_TD_root_id -N "" -qssh-keygen -t ed25519 -f ./guest_TD_root_id -N "" -qssh-keygen -t ed25519 -f ./guest_TD_root_id -N "" -q -
Download an Intel TDX-enabled cloud image and prepare it for use as a TD:
wget https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-x86_64-10-latest.x86_64.qcow2 \ -O centos.qcow2 virt-customize -a \ centos.qcow2 \ --root-password password:root \ --uninstall cloud-init \ --ssh-inject "root:file:./guest_TD_root_id.pub" qemu-img amend -o compat=v3 centos.qcow2wget https://download.opensuse.org/distribution/leap/16.0/appliances/Leap-16.0-Minimal-VM.x86_64-kvm-and-xen.qcow2 -O opensuse16.qcow2 virt-customize -a \ opensuse16.qcow2 \ --root-password password:root \ --uninstall jeos-firstboot \ --ssh-inject "root:file:./guest_TD_root_id.pub"To create a RHEL guest image, manually create a cloud ISO image. This guide uses a CentOS Stream 10 prebuilt cloud image:
wget https://cloud.centos.org/centos/10-stream/x86_64/images/CentOS-Stream-GenericCloud-x86_64-10-latest.x86_64.qcow2 \ -O centos.qcow2 virt-customize -a \ centos.qcow2 \ --root-password password:root \ --uninstall cloud-init \ --ssh-inject "root:file:./guest_TD_root_id.pub" qemu-img amend -o compat=v3 centos.qcow2To use a SLES guest image, go to the SUSE website and download a cloud image. This guide uses an openSUSE 16 cloud image:
wget https://download.opensuse.org/distribution/leap/16.0/appliances/Leap-16.0-Minimal-VM.x86_64-kvm-and-xen.qcow2 -O sles16.qcow2 virt-customize -a \ sles16.qcow2 \ --root-password password:root \ --uninstall jeos-firstboot \ --ssh-inject "root:file:./guest_TD_root_id.pub"wget https://cloud-images.ubuntu.com/releases/resolute/release/ubuntu-26.04-server-cloudimg-amd64.img -O ubuntu.img sudo qemu-img resize ubuntu.img +2G sudo virt-customize -a ubuntu.img \ --ssh-inject "root:file:./guest_TD_root_id.pub" \ --run-command 'growpart /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_hd0 1' \ --run-command 'resize2fs /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_hd0-part1' \ --run-command 'systemctl mask pollinate.service' \ --run-command 'cloud-init clean' # Create user-data and cloud-init image. cat > user-data.yaml << 'EOF' #cloud-config disable_root: false network: version: 2 ethernets: all-en: match: name: "en*" dhcp4: true dhcp-identifier: mac write_files: - path: /etc/ssh/sshd_config.d/99-td-enablement.conf content: | PermitRootLogin yes EOF # Use a unique instance-id so cloud-init always runs on first boot cat > meta-data.yaml << EOF instance-id: iid-$(uuidgen) local-hostname: td-guest EOF cloud-localds user-data.img user-data.yaml meta-data.yaml
Launch a Trust Domain¶
To launch a TD, use the following instructions for your distribution of choice:
(
# Check that the user is part of the `qgsd` group.
if ! groups | grep -qw "qgsd"; then
echo "User '$USER' is NOT in the 'qgsd' group. Attestation will fail to work in the TD."
exit 1
fi
# Find an unused IPv4 TCP port for SSH into the TD.
while :; do
SSH_PORT=$(shuf -i 49152-65535 -n 1)
# List listening IPv4 TCP sockets using the selected port.
# If no socket is listed, the port is available for QEMU to use.
ss -H -4 -ltn "sport = :$SSH_PORT" | grep -q . || break
done
/usr/libexec/qemu-kvm \
-accel kvm \
-smp 32 \
-m 16G \
-cpu host,-kvm-asyncpf-int \
-object '{"qom-type":"tdx-guest","id":"tdx","quote-generation-socket":{"type": "unix", "path":"/var/run/tdx-qgs/qgs.socket"}}' \
-object memory-backend-ram,id=mem0,size=16G \
-machine q35,kernel_irqchip=split,confidential-guest-support=tdx,memory-backend=mem0 \
-bios /usr/share/edk2/ovmf/OVMF.inteltdx.fd \
-nographic \
-nodefaults \
-vga none \
-drive file=${HOME}/centos.qcow2,if=none,id=virtio-disk0 \
-device virtio-blk-pci,drive=virtio-disk0 \
-device virtio-net-pci,netdev=nic0_td \
-netdev user,id=nic0_td,hostfwd=tcp::$SSH_PORT-:22 \
-monitor unix:/tmp/td-monitor.sock,server,nowait \
-daemonize && \
echo "Connect to TD via SSH: ssh -i ./guest_TD_root_id -p $SSH_PORT root@localhost"
)
(
# Check that the user is part of the `qgsd` group.
if ! groups | grep -qw "qgsd"; then
echo "User '$USER' is NOT in the 'qgsd' group. Attestation will fail to work in the TD."
exit 1
fi
# Find an unused IPv4 TCP port for SSH into the TD.
while :; do
SSH_PORT=$(shuf -i 49152-65535 -n 1)
# List listening IPv4 TCP sockets using the selected port.
# If no socket is listed, the port is available for QEMU to use.
ss -H -4 -ltn "sport = :$SSH_PORT" | grep -q . || break
done
qemu-kvm \
-accel kvm \
-smp 32 \
-m 16G \
-cpu host \
-object '{"qom-type":"tdx-guest","id":"tdx","quote-generation-socket":{"type": "unix", "path":"/var/run/tdx-qgs/qgs.socket"}}' \
-object memory-backend-ram,id=mem0,size=16G \
-machine q35,kernel_irqchip=split,confidential-guest-support=tdx,memory-backend=mem0 \
-bios /usr/share/qemu/ovmf-x86_64-tdx.bin \
-nographic \
-nodefaults \
-vga none \
-drive file=${HOME}/opensuse16.qcow2,if=none,id=virtio-disk0 \
-device virtio-blk-pci,drive=virtio-disk0 \
-device virtio-net-pci,netdev=nic0_td \
-netdev user,id=nic0_td,hostfwd=tcp::$SSH_PORT-:22 \
-monitor unix:/tmp/td-monitor.sock,server,nowait \
-daemonize
echo "Connect to TD via SSH: ssh -i ./guest_TD_root_id -p $SSH_PORT root@localhost"
)
(
# Check that the user is part of the `qgsd` group.
if ! groups | grep -qw "qgsd"; then
echo "User '$USER' is NOT in the 'qgsd' group. Attestation will fail to work in the TD."
exit 1
fi
# Find an unused IPv4 TCP port for SSH into the TD.
while :; do
SSH_PORT=$(shuf -i 49152-65535 -n 1)
# List listening IPv4 TCP sockets using the selected port.
# If no socket is listed, the port is available for QEMU to use.
ss -H -4 -ltn "sport = :$SSH_PORT" | grep -q . || break
done
/usr/libexec/qemu-kvm \
-accel kvm \
-smp 32 \
-m 16G \
-cpu host,-kvm-asyncpf-int \
-object '{"qom-type":"tdx-guest","id":"tdx","quote-generation-socket":{"type": "unix", "path":"/var/run/tdx-qgs/qgs.socket"}}' \
-object memory-backend-ram,id=mem0,size=16G \
-machine q35,kernel_irqchip=split,confidential-guest-support=tdx,memory-backend=mem0 \
-bios /usr/share/edk2/ovmf/OVMF.inteltdx.fd \
-nographic \
-nodefaults \
-vga none \
-drive file=${HOME}/centos.qcow2,if=none,id=virtio-disk0 \
-device virtio-blk-pci,drive=virtio-disk0 \
-device virtio-net-pci,netdev=nic0_td \
-netdev user,id=nic0_td,hostfwd=tcp::$SSH_PORT-:22 \
-monitor unix:/tmp/td-monitor.sock,server,nowait \
-daemonize && \
echo "Connect to TD via SSH: ssh -i ./guest_TD_root_id -p $SSH_PORT root@localhost"
)
(
# Check that the user is part of the `qgsd` group.
if ! groups | grep -qw "qgsd"; then
echo "User '$USER' is NOT in the 'qgsd' group. Attestation will fail to work in the TD."
exit 1
fi
# Find an unused IPv4 TCP port for SSH into the TD.
while :; do
SSH_PORT=$(shuf -i 49152-65535 -n 1)
# List listening IPv4 TCP sockets using the selected port.
# If no socket is listed, the port is available for QEMU to use.
ss -H -4 -ltn "sport = :$SSH_PORT" | grep -q . || break
done
qemu-kvm \
-accel kvm \
-smp 32 \
-m 16G \
-cpu host \
-object '{"qom-type":"tdx-guest","id":"tdx","quote-generation-socket":{"type": "unix", "path":"/var/run/tdx-qgs/qgs.socket"}}' \
-object memory-backend-ram,id=mem0,size=16G \
-machine q35,kernel_irqchip=split,confidential-guest-support=tdx,memory-backend=mem0 \
-bios /usr/share/qemu/ovmf-x86_64-tdx.bin \
-nographic \
-nodefaults \
-vga none \
-drive file=${HOME}/sles16.qcow2,if=none,id=virtio-disk0 \
-device virtio-blk-pci,drive=virtio-disk0 \
-device virtio-net-pci,netdev=nic0_td \
-netdev user,id=nic0_td,hostfwd=tcp::$SSH_PORT-:22 \
-monitor unix:/tmp/td-monitor.sock,server,nowait \
-daemonize
echo "Connect to TD via SSH: ssh -i ./guest_TD_root_id -p $SSH_PORT root@localhost"
)
(
# Check that the user is part of the `qgsd` group.
if ! groups | grep -qw "qgsd"; then
echo "User '$USER' is NOT in the 'qgsd' group. Attestation will fail to work in the TD."
exit 1
fi
# Find an unused IPv4 TCP port for SSH into the TD.
while :; do
SSH_PORT=$(shuf -i 49152-65535 -n 1)
# List listening IPv4 TCP sockets using the selected port.
# If no socket is listed, the port is available for QEMU to use.
ss -H -4 -ltn "sport = :$SSH_PORT" | grep -q . || break
done
qemu-system-x86_64 \
-accel kvm \
-smp 32 \
-m 16G \
-cpu host \
-object '{"qom-type":"tdx-guest","id":"tdx","quote-generation-socket":{"type": "unix", "path":"/var/run/tdx-qgs/qgs.socket"}}' \
-object memory-backend-ram,id=mem0,size=16G \
-machine q35,kernel_irqchip=split,confidential-guest-support=tdx,memory-backend=mem0 \
-bios /usr/share/ovmf/OVMF.inteltdx.ms.fd \
-nographic \
-nodefaults \
-vga none \
-drive file=ubuntu.img,if=none,id=virtio-disk0 \
-device virtio-blk-pci,drive=virtio-disk0 \
-drive file=user-data.img,if=none,id=cloud-init,format=raw \
-device virtio-blk-pci,drive=cloud-init \
-device virtio-net-pci,netdev=nic0_td \
-netdev user,id=nic0_td,hostfwd=tcp::$SSH_PORT-:22 \
-daemonize && \
echo "Connect to TD via SSH: ssh -i ./guest_TD_root_id -p $SSH_PORT root@localhost"
)
Once the TD has started, you can ssh into the TD and check that TDX is detected:
dmesg | grep -i tdx
The output should look similar to the following:
[ 0.000000] tdx: Guest detected
[ 0.000000] tdx: Attributes: SEPT_VE_DISABLE
[ 0.000000] tdx: TD_CTLS: PENDING_VE_DISABLE ENUM_TOPOLOGY VIRT_CPUID2 REDUCE_VE
[ 7.934806] process: using TDX aware idle routine
[ 7.934806] Memory Encryption Features active: Intel TDX
[ 7.934806] smpboot: CPU0: Intel TDX (family: 0x6, model: 0xad, stepping: 0x1)
[ 10.874294] systemd[1]: Detected confidential virtualization tdx.
[ 14.004275] systemd[1]: Detected confidential virtualization tdx.