negative zero

Setting up a Debian KVM virtual machine on a headless server with libvirt

2022 January 18

[debian] [tech] [tutorial]


This guide will refer to the physical machine as the host and the virtual machine installed in the host as the VM.

My host system while preparing this guide is a Debian Testing system without a GUI.

This guide does not use virt-manager, which requires a GUI.


Check CPU compatibility

Note: This step only applies to AMD and Intel CPUs. If you have an ARM, PowerPC, etc. CPU, I don't know how to tell if your CPU supports virtualization or not.

If your CPU doesn't support virtualization, you won't be able to follow this guide. Run this command to check:

user@host:~$ grep --color "svm\|vmx" /proc/cpuinfo
flags		: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov 
pat pse36 clflush mmx fxsr sse sse2 ht syscall nx mmxext fxsr_opt pdpe1gb rdtscp
 lm constant_tsc rep_good acc_power nopl nonstop_tsc cpuid extd_apicid aperfmper
f pni pclmulqdq monitor ssse3 cx16 sse4_1 sse4_2 movbe popcnt aes xsave avx f16c
 lahf_lm cmp_legacy svm extapic cr8_legacy abm sse4a misalignsse 3dnowprefetch o
svw ibs skinit wdt topoext perfctr_nb bpext ptsc perfctr_llc cpb hw_pstate ssbd 
vmmcall bmi1 xsaveopt arat npt lbrv svm_lock nrip_save tsc_scale flushbyasid dec
odeassists pausefilter pfthreshold overflow_recov

Assuming your CPU supports virtualization, this should output a list of flags including either svm (on AMD) or vmx (on Intel) which indicates virtualization support. If there's no output, your CPU does not have virtualization support enabled.

If you don't get any output, check your BIOS settings to see if virtualization is just disabled. If your CPU doesn't support it, you won't be able to follow this guide.


Load kernel module

Check if there's a KVM module already loaded:

root@host:~# lsmod | grep --color kvm
kvm_amd               143360  13
kvm                  1019904  1 kvm_amd
irqbypass              16384  14 kvm
ccp                   106496  1 kvm_amd

Look for either kvm_amd (if you have an AMD CPU) or kvm_intel (for Intel). If neither shows up, issue the appropriate command to load the one you need:

root@host:~# modprobe kvm_amd

or

root@host:~# modprobe kvm_intel


Dependencies

We need these packages:

  • qemu-kvm
  • qemu-utils
  • libvirt-clients
  • libvirt-daemon-system
  • virtinst
  • dnsmasq

In Debian, qemu-kvm is actually a virtual package. It will instead install the appropriate package based on your CPU, in my case qemu-system-x86.

root@host:~# apt update && apt install --no-install-recommends qemu-kvm qemu-utils libvirt-clients libvirt-daemon-system virtinst dnsmasq

(We're using --no-install-recommends here to avoid pulling in unnecessary graphical packages.)

Make sure libvirtd is running and enabled at start:

root@host:~# systemctl enable --now libvirtd

Download and verify a Debian iso

Go to https://www.debian.org/download to download the newest netinstall iso.

Also download the SHA512SUMS and Signature files to the same directory.

If you don't already have the Debian CD signing key, you can get it from keyring.debian.org:

user@host:~$ gpg --keyserver keyring.debian.org --recv-keys DF9B9C49EAA9298432589D76DA87E80D6294BE9B
gpg: /home/user/.gnupg/trustdb.gpg: trustdb created
gpg: key DA87E80D6294BE9B: public key "Debian CD signing key <debian-cd@lists.de
bian.org>" imported
gpg: Total number processed: 1
gpg:               imported: 1

Now, verify that the SHA512 sums were signed properly:

user@host:~$ gpg --verify-files SHA512SUMS.sign
gpg: assuming signed data in 'SHA512SUMS'
gpg: Signature made Sat Dec 18 00:00:00 2021 UTC
gpg:                using RSA key DF9B9C49EAA9298432589D76DA87E80D6294BE9B
gpg: Good signature from "Debian CD signing key <debian-cd@lists.debian.org>" [u
nknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg:          There is no indication that the signature belongs to the owner.
Primary key fingerprint: DF9B 9C49 EAA9 2984 3258  9D76 DA87 E80D 6294 BE9B

(Key line bolded for emphasis.)

The warning line says that this key is not certified with a trusted signature because I haven't been able to verify that the key is authentic. Unfortunately, that's not something I can reasonably do.

Now, confirm that the SHA512 hash of the iso you downloaded is correct:

user@host:~$ sha512sum --ignore-missing -c SHA512SUMS
debian-11.2.0-amd64-netinst.iso: OK

It should say the name of the file and that it's OK.


Create the VM

First, create the qcow2 file. The disk storage for the VM will be stored in this file.

root@host:~# qemu-img create -f qcow2 /var/lib/libvirt/images/my-vm-name.qcow2 10G

Replace "my-vm-name" and the size as needed.

Now here's the key step. We're going to create the VM, but we need to ensure that we can access it without needing to launch a GUI.

root@host:~# virt-install \
--name my-vm-name \
--description "Debian VM" \
--ram=1024 \
--vcpus=1 \
--cpu host \
--location /path/to/debian/iso \
--graphics none \
--disk path=/var/lib/libvirt/images/my-vm-name.qcow2,size=10 \
--extra-args 'console=ttyS0,115200n8 serial' \
--network default

The --graphics none and --extra-args 'console=ttyS0,115200n8 serial' enable us to start the Debian installer in the terminal without launching a GUI.

The Debian installer should start. Do the next section before going through the installer.


Assign the VM an IP address

Open a second terminal on the host.

You should have a "default virtual network" by default. To confirm this,

root@host:~# virsh net-list --all
 Name      State    Autostart   Persistent
--------------------------------------------
 default   active   yes         yes

If not, see the Networking page on the Libvirt Wiki.

When your VM is created, it should be assigned a MAC address. To get this MAC address, run

root@host:~# virsh dumpxml my-vm-name | grep "mac address"
      <mac address='52:54:00:b0:bc:b4'/>

Find subnet used by libvirt for VMs:

user@host:~$ ip a
[...]
10: virbr0:  mtu 1500 qdisc noqueue state UP 
group default qlen 1000
    link/ether 52:54:00:3f:5a:65 brd ff:ff:ff:ff:ff:ff
    inet 192.168.122.1/24 brd 192.168.122.255 scope global virbr0
       valid_lft forever preferred_lft forever
[...]

(Look for virbr0 or something similar.) In this example, your VMs will identify the host machine as 192.168.122.1, and you can assign other 192.168.122.x addresses to VMs.

Assign an IP address to the new VM:

root@host:~# virsh net-update default add ip-dhcp-host "<host mac='52:54:00:b0:bc:b4' name='my-vm-name' ip='192.168.122.2' />" --live --config

Install Debian

Now go back to the terminal running the Debian installer and walk through the installation process as normal. You should be able to configure networking, but you might have to manually specify it, for instance:

  • IP address: 192.168.122.2/24
  • Gateway: 192.168.122.1
  • Name server addresses: <whatever DNS server(s) you want to use>

What you install is up to you, but for this guide, you will only need the base system and an SSH server.

When the installer is finished, it should boot into a working Debian system. Make sure to set up ssh so you can connect to this VM easily!


Set up ssh

To ensure you can still connect to the VM after you set it up, make sure to set up ssh.

On the VM, install openssh-server if you didn't already:

root@vm:~# apt install openssh-server

It will generate keys and print their hashes.

Edit your sshd config:

root@vm:~# nano /etc/ssh/sshd_config

Uncomment and set the following two lines:

PubkeyAuthentication yes
PasswordAuthentication no

We want to use public key authentication, rather than password authentication. (Host-based authentication would be acceptable as well, since you should only be connecting from the host machine.)

Restart the ssh service to apply the new changes and ensure it's running:

root@vm:~# systemctl restart ssh

Also ensure ssh is enabled so it runs on startup:

root@vm:~# systemctl enable ssh

On the host device, generate an ssh key if you don't already have one:

user@host:~$ ssh-keygen -t ed25519
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/user/.ssh/id_ed25519): 
Created directory '/home/user/.ssh'.
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/user/.ssh/id_ed25519
Your public key has been saved in /home/user/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:+aot/GJT+/1KmKuJB2IJkBhmzbOOYEBRGqZj3oddUZk user@host
The key's randomart image is:
+--[ED25519 256]--+
|+X=.   ...o      |
|X o+    .E       |
|++  o  .         |
|+oo.o .  .       |
|o.o+ +  S        |
| . .= . .. o     |
|   . o o .+ .    |
|      *ooo +     |
|     .oB*oo oo.  |
+----[SHA256]-----+

(I've assumed defaults here, but you can change it if you want.)

Configure ssh on the host.

user@host:~$ nano ~/.ssh/config

Write a config something like this:

Host my-vm-name
	HostName 192.168.122.2
	User username
	Port 22
	IdentityFile ~/.ssh/id_ed25519
	PubkeyAuthentication yes
	ServerAliveInterval 120

Most of this should be clear. Change it as needed. The ServerAliveInterval mitigates an annoying issue where ssh sessions freeze up because one side disconnects without notifying the other.

For security reasons, ssh files must be readable only by the owner. Change the permissions accordingly:

user@host:~$ chmod 600 ~/.ssh/config

Output your public key:

user@host:~$ cat ~/.ssh/id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJy84ATUtHwlyepnXrU1vKsgfSmHRxmA5KOZlQLhhtg4
 user@host

You'll need this in a moment.

On the VM, authorize the host's key.

First, create a .ssh folder and an authorized_keys file and set the appropriate permissions:

user@vm:~$ mkdir -p ~/.ssh && chmod 700 ~/.ssh && touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys

Now, copy your public key from above into the authorized_keys file:

user@vm:~$ echo "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJy84ATUtHwlyepnXrU1vKsgfSmHRxmA5KOZlQLhhtg4 user@host" >> ~/.ssh/authorized_keys

(Using >> will append the key to the file. Using only one > will overwrite the file. You can have multiple authorized keys, and they should be one per line.)

Now, get the VM's key fingerprint:

user@vm:~$ ssh-keygen -lf <(ssh-keyscan -t ed25519 localhost 2>/dev/null)
256 SHA256:H5WgbU/P3K0m8deOvXTHdAArAPMlfdN1HJVroLNP2b8 localhost (ED25519)

You'll need this in a moment.

On the host, try to ssh into the VM:

user@host:~$ ssh my-vm-name
The authenticity of host '192.168.122.2 (192.168.122.2)' can't be established.
ED25519 key fingerprint is SHA256:H5WgbU/P3K0m8deOvXTHdAArAPMlfdN1HJVroLNP2b8.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])?

The first time you connect to a new host, ssh will ask you to verify its fingerprint.

Ensure that the fingerprint matches the one from above and enter yes. Alternatively, you can directly enter the fingerprint from before.

If all went well, you should now be logged into the VM over ssh.

To be extra safe, you should reboot the VM and ensure that you can still ssh in.

Once you've confirmed that you can ssh in even after a reboot, it's safe to disconnect from the terminal where you were originally setting up the VM. Press Ctrl + ] to disconnect.


Basic virsh commands

You can use the virsh utility to manage VMs. Here are some useful commands...

View all VMs:

root@host:~# virsh list --all
 Id   Name       State
--------------------------
 12   my-vm-name running

Start a VM:

root@host:~# virsh start my-vm-name

Shutdown a VM:

root@host:~# virsh shutdown my-vm-name

Reboot a VM:

root@host:~# virsh reboot my-vm-name

Force shutdown a VM:

root@host:~# virsh destroy my-vm-name

Delete a VM:

root@host:~# virsh undefine my-vm-name

Edit a VM:

root@host:~# virsh edit my-vm-name

Congratulations! You should now be running a Debian virtual machine which you can ssh into.

This VM will be accessible from the private IP address 192.168.122.2 or whatever you set it as. You can use this to reverse-proxy, etc.