2021-07-16
The WireGuard VPN challenge of provisioning clients
I mentioned in yesterday's entry that at work I'm building a VPN server that will support WireGuard. I'm quite happy with WireGuard in general and I think it has some important attractive features (such as the lack of 'sessions'), but we won't be offering WireGuard for general use. I would like to, but every time I even consider the idea, I run headlong into the problem of provisioning, specifically of provisioning WireGuard clients in some way that ordinary people can successfully set them up.
Right now, to set up a WireGuard client you need the server's name
and port (which every VPN needs), the server's public key, the IP
the server expects you to have inside the WireGuard connection (its
AllowedIPs
setting for you), and a private key that the server
has the public key for. We also
need you to set your DNS server(s) to correctly point to us, and
for general VPN usage you have to set your AllowedIPs
to 0.0.0.0/0.
This is a lot more things for you to set up than other VPN servers
need, partly because other VPN servers will push your internal IP,
the DNS servers to use, and often other information to you. Much
of this is also sensitive to typos or, in the case of keys, must
be cut and pasted to start with (no one is typing a base64 WireGuard
key). If you get your client IP wrong, for example, things just
quietly don't work (the server will discard your traffic).
The client keypair is an especially touchy problem. The ideal would
be to securely generate it on the client and upload the public key.
In practice this is asking a lot of people to do more or less by
hand, so in a realistic setup we would probably want to generate
your client keypair on the server and then somehow give you access
to the private key for you to configure along side the server's
public key. Given this, possibly the most generally usable way of
provisioning WireGuard client connections would be to generate the
wg.conf
that a client would use with the normal WireGuard command
line tools, then provide it to people and hope that any WireGuard
client will be able to import it.
(The official WireGuard client for iOS and Android will apparently
do this, including decoding the configuration from a QR code. I
believe the official Windows client does as well. On Unix, you can
use the wg.conf
directly or import it into NetworkManager.)
An additional complication is that you need a separate WireGuard configuration on each device that you want to use WireGuard on at the same time. So we wouldn't have to just provision one WireGuard setup per person, we're looking at one for your laptop, one for your phone, one for your tablet, and so on. This also complicates naming them and keeping track of them (for people and for us), and likely would tempt people into reusing configurations across devices, which leads to fun problems if both devices are in use at the same time.
I don't blame the WireGuard project for this state of affairs. Provisioning is both a hard problem and a high level concern that is sort of out of scope for a project that's deliberately low level and simple. I'm honestly impressed (and happy) that there are official WireGuard clients on as many platforms as there are. I do wish there was some officially supported way to push configuration information to clients, although I understand why there isn't.
(Tailscale is not a solution for us for various reasons, including price. I do admire them for solving the provisioning problem, though.)
Setting up a WireGuard client with NetworkManager (using nmcli
)
For reasons beyond the scope of this entry, I've been building a VPN server that will support WireGuard (along with OpenVPN and L2TP). A server needs a client, so I spent part of today setting up my work laptop as a WireGuard client in a 'VPN' configuration, under NetworkManager because that's what my laptop uses. I was hoping to do this through the Cinnamon GUIs for NetworkManager, but unfortunately while NetworkManager itself has supported WireGuard for some time, this support hasn't propagated into GUIs such as the GNOME Control Center (cf) or the NetworkManager applet that Cinnamon uses.
I'm already quite familiar with WireGuard in general, so I found
that the easiest way to start was to set up a basic WireGuard
configuration file for the connection in /etc/wireguard/wg0.conf
,
including both the main configuration (with the laptop's key and
my local port) and a [Peer]
section for the server. Since I'm
using WireGuard here in a VPN configuration, instead of to reach
just some internal IPs, I set AllowedIPs
to 0.0.0.0/0. After writing wg0.conf
, I then imported it into
NetworkManager:
nmcli connection import type wireguard file /etc/wireguard/wg0.conf
(For what can go in the configuration file, start with wg(8)
and wg-quick(8)
. I suspect
that NetworkManager doesn't support some of the more advanced keys.
I stuck to the basics. The import process definitely ignores the
various script settings supported by wg-quick(8)
. Currently,
see nm_vpn_wireguard_import()
in nm-vpn-helpers.c.)
Imported connections are apparently set to auto-connect, which isn't what I wanted, plus there were some other things to adjust (following the guide of Thomas Haller's WireGuard in NetworkManager):
nmcli con modify wg0 \ autoconnect no \ ipv4.method manual \ ipv4.address 172.29.50.10/24 \ ipv4.dns <...>
At this point you might be tempted to set ipv4.gateway
, and indeed
that's what I did the first time around. It turns out that this is
a mistake, because these days NetworkManager will do the right thing
based on the 'accept everything' AllowedIPs
I set, right down to
setting up policy based routing with a fwmark so that encrypted
traffic to the WireGuard VPN server doesn't try to go over WireGuard.
If you set ipv4.gateway
as well, you wind up with two default
routes and then your encrypted WireGuard traffic may try to go over
your WireGuard connection again, which doesn't work.
(See the description of 'ip4-auto-default-route
in the WireGuard
configuration properties.
The full index of available NetworkManager settings in various
sections is currently here; the
ones most useful to me are probably connection.*
and ipv4.*
.)
Getting DNS to work correctly requires a little extra step, or at
least did for me. While the wg0
connection is active, I want all
of my DNS queries to go to our internal resolving DNS server and
also to have a search path of our university subdomain. This
apparently requires explicitly including '~
' in the NetworkManager
DNS search path:
nmcli con modify wg0 \ ipv4.dns-search "cs.toronto.edu,~"
This comes from Fedora bug #1895518, which also
has some useful resolvectl
options.
You (I) can see a lot of settings for the WireGuard setup with
'nmcli connection show wg0
', including active ones, but this seems
to omit NetworkManager's view of the WireGuard peers. To see that,
I needed to look directly at the configuration file that NetworkManager
wrote, in /etc/NetworkManager/system-connections/wg0.nmconnection
.
I'm someday going to need to edit this directly to modify the
WireGuard VPN server's endpoint from my test machine to the production
machine.
(The NetworkManager RFE for configuring WireGuard peers in nmcli
is issue #358.)
With no GUI support for WireGuard connections, I have to bring this
WireGuard VPN up and down with 'nmcli con up wg0
' and 'nmcli con
down wg0
'. Once I have the new VPN server in production, I'll be
writing little scripts to do this for me. Hopefully this will be
improved some day, so that the NetworkManager applet allows you to
activate and deactivate WireGuard connections and shows you that
one is active.
If I wanted a limited VPN that only sent traffic to our internal
networks over my WireGuard link, I would configure the server's
AllowedIPs
to the list of networks and then I believe that
NetworkManager would automatically set up routes for them. However,
I don't know how to make this work (in NetworkManager) if the
WireGuard VPN server itself was on one of the subnets I wanted to
reach over WireGuard. For my laptop, routing all traffic over
WireGuard to work is no worse than using our OpenVPN or L2TP VPN
servers, which also do the same thing by default.
(On my home desktop, I use hand built fwmark-based policy rules to deal with my WireGuard endpoint being on a subnet I want to normally reach over WireGuard. NetworkManager will build the equivalents for me when I'm routing 0.0.0.0/0 over the WireGuard link, but I believe not in other situations.)
(For information, I primarily relied on Thomas Haller's WireGuard in NetworkManager, supplemented with a Fedora Magazine article and this article.)