I created a VM using KVM on an Ubuntu PC at home. To allow SSH access to the VM from external hosts, I connected the VM to a bridge network created on the host. I ran into several issues along the way, so I’m documenting the entire procedure here.

The Japanese version of this article is available here.

Environment

Host OS

Ubuntu 22.04.1 LTS

Guest OS

Ubuntu 20.04.5 LTS

Procedure

I’ll skip the steps for installing KVM on Ubuntu (refer to the official instructions).

Create a Bridge

I referenced this article.

Remove the Default Network

When you install KVM, a bridge called virbr0 should already exist by default. This bridge uses NAT to allow the VM to access external networks. With this bridge, communication between the host and VM is possible, but external hosts cannot access the VM. Since we don’t need it for our purposes, let’s remove it.

The default configuration is registered as “default” in KVM, so we delete it.

$ virsh net-destroy default
$ virsh net-undefine default

Create a Bridge with netplan and Attach the NIC

We’ll use netplan. Edit an existing file or create a new one under /etc/netplan. The host NIC enxf8e43bb371e8 has a static IP address of 192.168.0.130, so we’ll use that address.

# /etc/netplan/99_config.yaml
network:
  version: 2
  ethernets:
    enxf8e43bb371e8:
      dhcp4: false
      dhcp6: false
  bridges:
    br0:
      interfaces: [enxf8e43bb371e8]
      addresses: [192.168.0.130/24]
      gateway4: 192.168.0.1
      nameservers:
        addresses: [192.168.0.1, 8.8.8.8]
      parameters:
        stp: false
      dhcp4: false
      dhcp6: false
$ sudo netplan apply

This creates the new bridge br0. The IP address that was previously assigned to enxf8e43bb371e8 now appears under br0. Also, the master br0 label on the enxf8e43bb371e8 entry confirms that the NIC is properly connected to the bridge.

$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enxf8e43bb371e8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel master br0 state UP group default qlen 1000
    link/ether f8:e4:3b:b3:71:e8 brd ff:ff:ff:ff:ff:ff

(skip)

16: br0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 72:e2:b6:e9:05:35 brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.130/24 brd 192.168.0.255 scope global noprefixroute br0
       valid_lft forever preferred_lft forever
    inet6 fe80::70e2:b6ff:fee9:535/64 scope link
       valid_lft forever preferred_lft forever

Register the bridge with KVM. Create the following host-bridge.xml file.

<network>
  <name>host-bridge</name>
  <forward mode="bridge"/>
  <bridge name="br0"/>
</network>

Register it with KVM.

$ virsh net-define host-bridge.xml
$ virsh net-start host-bridge
$ virsh net-autostart host-bridge

Verify that it’s registered correctly.

 $ virsh net-list --all
 Name          State    Autostart   Persistent
------------------------------------------------
 host-bridge   active   yes         yes

Disable Bridge Netfilter

Addendum on June 12, 2024 The root cause of the VM not being able to reach external networks was that iptables was set to DENY the FORWARD chain. Therefore, this issue can also be resolved by changing the default policy for FORWARD to ACCEPT.

This is where I got stuck. Even after creating the VM in the next step, it couldn’t reach external networks. The cause was Bridge Netfilter blocking traffic through the bridge. Following this article, I disabled Bridge Netfilter on the host.

Add the following line to /etc/sysctl.conf. (Depending on your environment, a similar setting may already exist in /etc/sysctl.conf or /etc/sysctl.d/*, so be sure to check.)

net.bridge.bridge-nf-call-iptables = 0

Apply the changes.

$ sudo sysctl -p

Verify the update was applied correctly.

$ sudo sysctl net.bridge.bridge-nf-call-iptables
net.bridge.bridge-nf-call-iptables = 0

Create a VM

Create a VM with Ubuntu 20.04. Adjust the memory, CPU cores, disk size, etc. as needed.

$ virt-install \
  --name vm1 \
  --ram=2048 \
  --disk size=10 \
  --network network=host-bridge \
  --vcpus 1 \
  --os-type linux \
  --os-variant ubuntu20.04 \
  --graphics none \
  --location 'http://archive.ubuntu.com/ubuntu/dists/focal/main/installer-amd64/' \
  --extra-args "netcfg/disable_autoconfig=true console=ttyS0,115200n8 --- console=ttyS0,115200n8"

An important note here is the console=ttyS0,115200n8 --- console=ttyS0,115200n8 part in --extra-args. If you’re doing everything via CLI, you’ll need to connect to the VM later with virsh console vm1 to configure SSH and other settings. Without enabling serial console access, you won’t be able to interact with the VM at all. For more details, see this article.

Also, netcfg/disable_autoconfig=true prevents the installer from automatically assigning an address via DHCP during installation. Whether this is necessary depends on your environment, but in my case I wanted to assign a static address, so I used this option. (Reference) I looked into whether it was possible to specify a static address at this point, and while it seems possible on RHEL, I couldn’t find a way to do it on Ubuntu.

Ubuntu Installation

An installation wizard will appear, so follow the on-screen instructions to complete the installation.

Configure SSH

After installation, if the console connection is still active, you can proceed directly. Otherwise, connect manually.

$ virsh console vm1

Install the SSH server and register your keys.

$ sudo apt update
$ sudo apt install openssh-server

$ mkdir ~/.ssh
$ wget "https://github.com/hiroyaonoe.keys" -O ~/.ssh/authorized_keys

After exiting, you should be able to SSH in using the username and IP address you specified during installation (if using DHCP, look up the assigned address accordingly).

Conclusion

Networking configuration is tricky. In particular, the Bridge Netfilter issue took a long time to resolve, so I hope this helps someone. Now that I have VM infrastructure set up on my home Ubuntu PC, I’d like to try building a Kubernetes cluster on it.