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
- “infrastructure” sounds impressive, this whole thing ran on the Raspberry Pi 4 that is my house’s wiring closet conencted to the uplink switch.