I got super, super interested in WireGuard when Linus Torvalds heaped fulsome praise on its design (if you’re not familiar with Linus’ commentary, then trust me – that’s extremely fulsome in context) in an initial code review this week. WireGuard aims to be more secure and faster than competing VPN solutions; as far as security goes, it’s certainly one hell of a lot more auditable, at 4,000 lines of code compared to several hundred thousand lines of code for OpenVPN/OpenSSL or IPSEC/StrongSwan.
I’ve got a decade-and-a-half of production experience with OpenVPN and various IPSEC implementations, and “prettiness of code” aside, frankly they all suck. They’re not so bad if you only work with a client or ten at a time which are manually connected and disconnected; but if you’re working at a scale of hundred+ clients expected to be automatically connected 24/7/365, they’re a maintenance nightmare. The idea of something that connects quicker and cleaner, and is less of a buggy nightmare both in terms of security and ongoing usage, is pretty strongly appealing!
WARNING: These are my initial testing notes, on 2018-Aug-05. I am not a WireGuard expert. This is my literal day zero. Proceed at own risk!
Alright, so clearly I wanna play with this stuff. I’m an Ubuntu person, so my initial step is apt-add-repository ppa:wireguard/wireguard ; apt update ; apt install wireguard-dkms wireguard-tools
.
After we’ve done that, we’ll need to generate a keypair for our wireguard instance. The basic commands here are wg genkey
and wg pubkey
. You’ll need to pipe private key created with wg genkey
into wg pubkey
to get a working private key. You don’t have to store your private key anywhere outside the wg0.conf
itself, but if you’re a traditionalist and want them saved in nice organized files you can find (and which aren’t automagically monkeyed with – more on that later), you can do so like this:
root@box:/etc/wireguard# touch machinename.wg0.key ; chmod 600 machinename.wg0.key
root@box:/etc/wireguard# wg genkey > machinename.wg0.key
root@box:/etc/wireguard# wg pubkey < machinename.wg0.key > machinename.wg0.pub
You’ll need this keypair to connect to other wireguard machines; it’s generated the same way on servers or clients. The private key goes in the [Interface]
section of the machine it belongs to; the public key isn’t used on that machine at all, but is given to machines it wants to connect to, where it’s specified in a [Peer]
section.
From there, you need to generate a wg0.conf
to define a wireguard network interface. I had some trouble finding definitive information on what would or wouldn’t work with various configs on the server side, so let’s dissect a (fairly) simple one:
# /etc/wireguard/wg0.conf - server configs
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = SERVER_PRIVATE_KEY
# SaveConfig = true makes commenting, formattting impossible
SaveConfig = false
# This stuff sets up masquerading through the server's WAN,
# if you want to route all internet traffic from your client
# across the Wireguard link.
#
# You'll also need to set net.ipv4.ip_forward=1 in /etc/sysctl.conf
# if you're going this route; sysctl -p to reload sysctl.conf after
# making your changes.
#
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE; ip6tables -A FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE; ip6tables -D FORWARD -i wg0 -j ACCEPT; ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
OK, so far so good. Note that SERVER_PRIVATE_KEY
above is not a reference to a filename – it’s the server’s private key itself, pasted directly into the config file!
With the above server config file (and a real private key on the private key line), wg0 will start, and will answer incoming connections. The problem is, it’ll answer incoming connections from anybody who has the server’s public key – no verification of the client necessary. (TESTED)
Here’s a sample client config:
# client config - client 1 - /etc/wireguard/wg0.conf
[Interface]
Address = 10.0.0.2/20
SaveConfig = true
PrivateKey = MY_PRIVATE_KEY
# Warning: setting DNS here won't work if you don't
# have resolvconf installed... and if you're running
# Ubuntu 18.04, you probably don't have resolvconf
# installed. If you set this without resolvconf available,
# the whole interface will fail to come up.
#
# DNS = 1.1.1.1
[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = wireguard.mydomain.wtflol:51820
# this restricts tunnel traffic to the VPN server itself
AllowedIPs = 172.29.128.1/32
# if you wanted to route ALL traffic across the VPN, do this instead:
# AllowedIPs = 0.0.0.0/0
Notice that we set SaveConfig=true
in wg0.conf
here on our client. This may be more of a bug than a feature. See those nice helpful comments we put in there? And notice how we specified an FQDN instead of a raw IP address for our server endpoint? Well, with SaveConfig=true
on, those are going to get wiped out every time the service is restarted (such as on boot). The comments will just get wiped, stuff like the random dynamic port the client service uses will get hard-coded into the file, and the FQDN will be replaced with whatever IP address it resolved to the last time the service was started.
So, yes, you can use an FQDN in your configs – but if you use SaveConfig=true
you might as well not bother, since it’ll get immediately replaced with a raw IP address anyway. Caveat imperator.
If we want our server to refuse random anonymous clients and only accept clients who have a private key matching a pubkey in our possession, we need to add [Peer]
section(s):
[Peer]
PublicKey = PUBLIC_KEY_OF_CLIENT_ONE
AllowedIPs = 10.0.0.2/32
This works… and with it in place, we will no longer accept connections from anonymous clients. If we haven’t specifically authorized the pubkey for a connecting client, it won’t be allowed to send or receive any traffic. (TESTED.)
We can have multiple peers defined, and they’ll all work simultaneously, on the same port on the same server: (TESTED)
# appended to wg0.conf on SERVER
[Peer]
PublicKey = PUBKEY_OF_CLIENT_ONE
AllowedIPs = 10.0.0.2/32
[Peer]
PublicKey = PUBKEY_OF_CLIENT_TWO
AllowedIPs = 10.0.0.3/32
Wireguard won’t dynamically reload wg0.conf
looking for new keys, though; so if we’re adding our new peers manually to the config file like this we’ll have to bring the wg0 interface down and back up again to load the changes, with wg-quick down wg0 && wg-quick up wg0
. This is definitely not a good way to do things in production at scale, because it means approximately 15 seconds of downtime for existing clients before they automatically reconnect themselves: (TESTED)
64 bytes from 172.29.128.1: icmp_seq=10 ttl=64 time=35.0 ms
64 bytes from 172.29.128.1: icmp_seq=11 ttl=64 time=39.3 ms
64 bytes from 172.29.128.1: icmp_seq=12 ttl=64 time=37.6 ms
[[[ client disconnected due to server restart ]]]
[[[ 16 pings dropped ==> approx 15-16 seconds downtime ]]]
[[[ client automatically reconnects itself after timeout ]]]
64 bytes from 172.29.128.1: icmp_seq=28 ttl=64 time=51.4 ms
64 bytes from 172.29.128.1: icmp_seq=29 ttl=64 time=37.6 ms
A better way to do things in production is to add our clients manually with the wg
command itself. This allows us to dynamically add clients without bringing the server down, and that doing so will also add those clients into wg0.conf
for persistence across reboots and what-have-you.
If we wanted to use this method, the CLI commands we’ll need to run on the server look like this: (TESTED)
root@server:/etc/wireguard# wg set wg0 peer CLIENT3_PUBKEY allowed-ips 10.0.0.4/24
The client CLIENT3
will immediately be able to connect to the server after running this command; but its config information won’t be added to wg0.conf
, so this isn’t a persistent addition. To make it persistent, we’ll either need to append a [Peer]
block for CLIENT3
to wg0.conf manually, or we could use wg-quick save wg0
to do it automatically. (TESTED)
root@server:/etc/wireguard# wg-quick save wg0
The problem with using wg-quick save
(which does not require, but shares the limitations of, SaveConfig = true
in the wg0.conf
itself) is that it strips all comments and formatting, permanently resolves FQDNs to raw IP addresses, and makes some things permanent that you might wish to keep ephemeral (such as ListenPort
on client machines). So in production at scale, while you will likely want to use the wg set
command to directly add peers to the server, you probably won’t want to use wg-quick save
to make the addition permanent; you’re better off scripting something to append a well-formatted [Peer]
block to your existing wg0.conf
instead.
Once you’ve gotten everything working to your liking, you’ll want to make your wg0 interface come up automatically on boot. On Ubuntu Xenial or later, this is (of course, and however you may feel about it) a systemd thing:
root@box:/etc/wireguard# systemctl enable wg-quick@wg0
This is sufficient to automatically bring up wg0
at boot; but note that since we’ve already brought it up manually with wg-quick up
in this session, an attempt to systemctl status wg-quick@wg0
will show an error. This is harmless, but if it bugs you, you’ll need to manually bring wg0 down, then start it up again using systemctl
:
root@box:/etc/wireguard# wg-quick down wg0
root@box:/etc/wireguard# systemctl start wg-quick@wg0
At this point, you’ve got a working wireguard interface on server and client(s), that’s persistent across reboots (and other disconnections) if you want it to be.
What we haven’t covered
Note that we haven’t covered getting packets from CLIENT1 to CLIENT2 here – if you try to communicate directly between two clients with this setup and no additional work, you’ll see the following error: (TESTED)
root@client1:/etc/wireguard# ping -c1 CLIENT2
From 10.0.0.2 icmp_seq=1 Destination Host Unreachable
ping: sendmsg: Required key not available
--- 10.0.0.3 ping statistics ---
1 packets transmitted, 0 received, +1 errors, 100% packet loss, time 0ms
We also haven’t looked around at any kind of crypto configuration yet; at this point we’re blindly accepting whatever defaults for algorithms, key sizes, and so forth and hoping for the best. Make sure you understand these (and I don’t, yet!) before deploying in production.
At this point, though, we’ve at least got something working we can play with. Happy hacking, and good luck!