Running systemd-nspawn containers with a VPN interface

Recently, there was a project where someone needed to do some measurements using my infrastructure (1). They just needed a “machine” that they could connect to over wireguard that also had an internet connection. Naturally, I wanted to run said “machine” in systemd-nspawn. With just a few lines of configuration for a few systemd components, we can create a container that takes a VPN interface from the host, configures an address on it so the container can be accessed via that VPN.

The container is called ubuntu-focal-interfaces and the wireguard interface is called wg1.

Here’s the steps needed to accomplish that.

Adding the Wireguard interface on the host with systemd-networkd

If you’re not using systemd-networkd on the host machine, just create a wireguard interface as described in the wireguard docs but don’t set an IP address on the interface.

systemd-networkd can create wireguard interfaces itself using a .netdev file. Create one in /etc/systemd/network/wg1.netdev:

[NetDev]
Name=wg1
Kind=wireguard
Description=Wireguard client interface, used in a container

[WireGuard]
# Bonus points if you actually use PrivateKeyFile
PrivateKey=XXXXXX
ListenPort=51820

[WireGuardPeer]
Endpoint=some-host.example.com:51820
PublicKey=YYYYYY
AllowedIPs=192.0.2.0/24

# Ensure the NAT knows about this connection
PersistentKeepalive=60

As it contains private keys, systemd-networkd will refuse to load it if the permissions are too wide, so

sudo chown root:systemd-network /etc/systemd/network/wg1.netdev
sudo chmod 640 /etc/systemd/network/wg1.netdev`.

Now create the interface:

sudo networkctl reload

Creating the image

Now we actually need to run a container, for this we need an image that nspawn can run. Fortunately, mkosi has us covered. Download/install it and create a directory where we’ll add the image build config.

Now create a mkosi.default file:

[Distribution]
Distribution=ubuntu
Release=focal

[Packages]
Packages=iproute2

We’ll bake in the setting of the IP address on the interface into the image, mkosi.postinst:

#!/bin/bash
systemctl enable systemd-networkd
echo "[Match]
Name=wg1

[Network]
Address=192.0.2.2/24" > /etc/systemd/network/wg1.network

Now build the image and import it into machined:

chmod +x mkosi.postinst
sudo mkosi
# wait......
sudo machinectl import-raw image.raw ubuntu-focal-interfaces

Adding the right interfaces to the container

Configure nspawn to pass the wg1 interface to the container and set up a veth interface as well. Create a file /etc/systemd/nspawn/ubuntu-focal-interfaces.nspawn

[Network]
VirtualEthernet=true
Interface=wg1

Boot the container

Now start the container:

sudo machinectl start ubuntu-focal-interfaces

If you now run ip address show on the host, you’ll see the wg1 interface is gone, as it has moved to the container’s network namespace.

When you open a shell in the container (sudo machinectl shell ubuntu-focal-interfaces) and run ip address show there, you’ll see two interfaces, one veth interface that has internet access and one wg1 interface with the configured address.

Should you stop the container (sudo machinectl stop ubuntu-focal-interfaces), the wg1 interface is moved back into the host’s namespace. The IP address, however, is no longer configured on the interface. This means I can now now safely tell the people who need access the public key of the wireguard interface, start the container, install SSH and give them access to do whatever they need to do.

Security Considerations

Should the other party have root permissions inside your container, they can still edit the wireguard interface’s parameters. These changes will be lost once the host is rebooted of course, but keep this in mind when moving interfaces between namespaces and containers.

Footnotes

  1. “infrastructure” sounds impressive, this whole thing ran on the Raspberry Pi 4 that is my house’s wiring closet conencted to the uplink switch.

557 Words

2020-09-21 11:15 +0200