WireGuard VPN for remote working

What is a VPN?

My colleague, Sandro, has blogged previously about VPNs. It’s an excellent primer, so if you’re new to VPNs go and read his article first.

The market for all types of VPN is dominated by two solutions, IPSec and OpenVPN, but there’s now a new entrant making inroads.

WireGuard

Introducing WireGuard

WireGuard takes a different approach to both IPSec and OpenVPN.

IPSec compromises of a suite of protocols that allows authentication and encryption of data across a virtual tunnel. It’s infinitely highly configurable, and that’s a weakness of IPSec. It’s too configurable offering the ability to tweak and tune every aspect of the VPN. With this level of complexity it’s not surprising to find that vendors often have slightly different and slightly incompatible implementations of IPSec. This can make IPSec a frustrating experience to get going, but on the whole, once configured and running, IPSec tunnels are reliable and fast.

OpenVPN is an SSL/TLS open source based VPN solution. While it’s much simpler to implement than IPSec, it still offers a wide range of configurable options. OpenVPN uses an optional plugin system for authentication that can let you add UNIX user auth, require MFA tokens to be presented along with the option of running a script for the auth process allowing arbitrary authentication schemes. OpenVPN runs in userland outside of the kernel and typically performs an order of magnitude slower than IPSec although work is in-progress upstream to mitigate this by moving parts of OpenVPN into the kernel.

WireGuard is a VPN stripped back to the bare bones. It follows the KISS principle. It leverages existing constructs in the Linux networking stack and simply adds a new network interface. The way traffic is managed to or from that interface is handled using existing tooling such as the ip suite of commands.

WireGuard offers no configuration choices around the cryptography used, and this is an intentional design choice. Why offer the user the ability to choose which protocols are used for data encryption when it’s highly likely the end user isn’t a cryptographer? The choices of cryptography used are hard coded and some see this as a disadvantage because any weaknesses discovered in the protocols used would require all servers and clients to be upgraded. I see this as an advantage as it forces users of WireGuard to upgrade their systems if a weakness is discovered.

WireGuard doesn’t do logins. Traffic is secured between peers using private/public key pairs, and optionally an extra pre-shared key. If both ends know their private keys and agree on each other’s identity, packets flow (this is similar to IPSec in “infrastructure” mode). Again, this was an intentional design choice to keep the implementation simple. If extra layers of authentication are required then these can be implemented in other layers of the stack.

The code base is intentionally small, running to less than 4000 lines of code. This makes it much easier to perform security audits on the codebase even by individuals. Having less code also means there’s less chance of bugs.

WireGuard offers extremely good performance. On Linux systems, WireGuard runs entirely within the kernel and can easily saturate gigabit network links on very modest hardware. Implementations on other platforms vary between kernel and userspace with the latter implementations having less performance, but still vastly outperforming OpenVPN solutions.

As there’s no need to establish a tunnel before sending data unlike IPSec or OpenVPN, it’s possible for WireGuard to work seamlessly when roaming between network links, switch IP addresses or on unreliable and slow connections. This is really noticeable in the real world particularly if you’ve experienced the pain of attempting to use OpenVPN while travelling on a train with an intermittent connection.

So how does WireGuard work?

WireGuard treats every endpoint as a ‘peer’. Each peer has a unique public and private key pair that uniquely identifies that peer. Each peer connects to another peer in a point to point fashion. To authenticate each peer is configured with the opposite peer’s public key. The private keys must remain secret and should be stored securely.

Although WireGuard treats all endpoints as peer, for the purpose of this demonstration, I’m going to refer to a ‘server’ and a ‘client’ as that’s the terminology most people are most familiar with. But remember, as far as WireGuard is concerned, these are both simply ‘peers’.

Getting started

First of all we need to check if we need to install WireGuard. The Linux kernel merged WireGuard into Linux 5.6, so if you’re running a kernel version of 5.6 or above then you already have WireGuard support, built-in. For everyone else you’ll need to install WireGuard. Most platforms have WireGuard packages available so check your package manager. For Debian based distributions installing wireguard-dkms will install and build the kernel module along with the necessary tools package.

Key pairs

Let’s start by generating a key pair on the server:

wg genkey | tee server_private.key | wg pubkey > server_public.pub

Then we repeat the process on the client:

wg genkey | tee client_private.key | wg pubkey > client_public.pub

The keys will look something like this on the server:

[paul@server ~]$ wg genkey | tee server_private.key | wg pubkey > server_public.pub
[paul@server ~]$ cat server_private.key
0F2aBt7aFGAkgSPsRpS16n4CBo4jpXprfrvf2lqEfUE=
[paul@server ~]$ cat server_public.pub
kTtL3zavwqkwurJhOD/Z3Vm2p7+5YdfiT7O+IDTJ/jU=
[paul@server ~]$

..and something like this on the client:

[paul@client ~]$ wg genkey | tee client_private.key | wg pubkey > client_public.pub
[paul@client ~]$ cat client_private.key
GDANe71DbA3F14A9kfUHjOJCPixrubnyXkn9k3UaqUQ=
[paul@client ~]$ cat client_public.pub
sP9Exwv1EDSqiO58ulrwXk61cNz1hjtgwYx7XdvXEz4=

Setup

Now we need to pass the setup information to WireGuard. This can be done manually at the command line or using one of the helper tools such as wg-quick. We’ll use the helper tool as that’s the most common way of interacting with WireGuard tunnels and it’s supported across Debian and RedHat based distributions.

On the server, if it doesn’t exist already, create a /etc/wireguard directory and then create a new file called wg0.conf inside that directory. This uses the standard INI file format.

Let’s build up the wg0.conf file. Firstly we define an ‘Interface’. This provides the configuration for the server.

[Interface]
ListenPort = 51820
PrivateKey = 0F2aBt7aFGAkgSPsRpS16n4CBo4jpXprfrvf2lqEfUE=
Address = 192.168.192.1/24

The default WireGuard port is 51820 but you can change this using the ListenPort setting. WireGuard uses UDP for all communications. We specify the content of the server_private.key as the value to PrivateKey.

Next we specify a list of peers that we want to talk to, in our case a single peer, ‘client’:

# client.example.org
[Peer]
PublicKey = sP9Exwv1EDSqiO58ulrwXk61cNz1hjtgwYx7XdvXEz4=
AllowedIPs = 192.168.192.2/32

WireGuard doesn’t (yet) have an in-built mechanism for identifying peers other than through the key pairs so it’s good practice, for now, to comment each peer with some identifying label to help you identify the remote peer at a later date. The PublicKey specified here is that public key belonging to the remote device, in this case, the client. The AllowedIPs setting acts as a firewall and restricts what traffic will be allowed in or out of that peer. In this example, we only allow traffic to or from the IP address 192.168.10.2 which is the IP address we’ll assign to the peer.

If you’ve been following along at home, you now have everything in-place to bring up one end of a WireGuard VPN. Use the command wg-quick up wg0 to bring the VPN up. We can check the status of the VPN with the wg command; the output is similar to:

interface: wg0
  public key: kTtL3zavwqkwurJhOD/Z3Vm2p7+5YdfiT7O+IDTJ/jU=
  private key: (hidden)
  listening port: 51820

peer: sP9Exwv1EDSqiO58ulrwXk61cNz1hjtgwYx7XdvXEz4=
  allowed ips: 192.168.192.2/32

This shows that we have a WireGuard VPN configured with a single peer, but without any traffic currently flowing. Let’s remedy that by setting up a remote peer.

On the client, I’ll create a new /etc/wireguard/wg0.conf file with the following content:

[Interface]
PrivateKey = GDANe71DbA3F14A9kfUHjOJCPixrubnyXkn9k3UaqUQ=
Address = 192.168.192.2/24

# server.example.org
[Peer]
PublicKey = kTtL3zavwqkwurJhOD/Z3Vm2p7+5YdfiT7O+IDTJ/jU=
AllowedIPs = 192.168.192.0/24
Endpoint = 203.0.113.52:51820
PersistentKeepAlive = 25

As with the server, we specify an interface block and provide an address for the client and the private key. In the peer block we provide the public key of the server. This time we use AllowedIPs to inform the client that this peer will handle traffic for the 192.168.192.0/24. This results in wg-quick automatically updating the routing table appropriately. The Endpoint provides the address and port of the server, you’ll need to change this to match your servers IP address which must be reachable from the client.

The PersistentKeepAlive can be useful when one side is on a dynamic IP such as the client in the example. It causes the client to send a ‘keep alive’ packet every 25 seconds which ensures that the tunnel remains active. Without this, the server would be unable to send data to the client through the tunnel without the client sending data first (which would inform the server of the client’s current IP address).

Testing

We now have everything ready so let’s bring up the client side of the VPN with wg-quick up wg0. You can view the resulting configuration by running wg. Let’s check that everything is working by using ping:

[paul@client ~]$ ping -c3 192.168.192.1
PING 192.168.192.1 (192.168.192.1) 56(84) bytes of data.
64 bytes from 192.168.192.1: icmp_seq=1 ttl=64 time=20.8 ms
64 bytes from 192.168.192.1: icmp_seq=2 ttl=64 time=20.9 ms
64 bytes from 192.168.192.1: icmp_seq=3 ttl=64 time=20.9 ms

--- 192.168.192.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 6ms
rtt min/avg/max/mdev = 20.833/20.872/20.902/0.121 ms

And again from the server side:

[paul@server ~]$ ping -c3 192.168.192.2
PING 192.168.192.2 (192.168.192.2) 56(84) bytes of data.
64 bytes from 192.168.192.2: icmp_seq=1 ttl=64 time=20.8 ms
64 bytes from 192.168.192.2: icmp_seq=2 ttl=64 time=21.3 ms
64 bytes from 192.168.192.2: icmp_seq=3 ttl=64 time=20.8 ms

--- 192.168.192.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 5ms
rtt min/avg/max/mdev = 20.751/20.947/21.287/0.268 ms

If we run wg again on the server we can now see evidence of activity:

interface: wg0
  public key: kTtL3zavwqkwurJhOD/Z3Vm2p7+5YdfiT7O+IDTJ/jU=
  private key: (hidden)
  listening port: 51820

peer: sP9Exwv1EDSqiO58ulrwXk61cNz1hjtgwYx7XdvXEz4=
  endpoint: 198.51.100.99:42539
  allowed ips: 192.168.192.2/32
  latest handshake: 1 minute, 7 seconds ago
  transfer: 1.21 KiB received, 1.12 KiB sent

The output from my PC shows that it last spoke to the client at the address 198.51.100.99 on port 42539. WireGuard will automatically and seamlessly update this address if the client roams to another IP address, or if its traffic switches to a different UDP port.

Let’s have a quick look at the routing table on the server, using ip route show:

default via 10.0.0.1 dev eth0
10.0.0.0/24 dev eth0 proto kernel scope link src 10.0.0.86
192.168.192.0/24 dev wg0 proto kernel scope link src 192.168.192.1

This shows that WireGuard has correctly added a new route for traffic to flow through the wg0 interface.

With the VPN now established, any remaining setup such as extra routing/forwarding and firewalling is handled using the standard operating system tools such as iptables and ip route. WireGuard, by design, only handles the layer-3 interface layer and it does it extremely well.

Beyond the basics

This example explained a common way to use WireGuard: a VPN service that remote workers can use. I’ve used WireGuard to access resources on a private network and also, pre-pandemic, to cut out the effects from wifi systems that intercept traffic (in the UK, train internet services often do this and it’s a pain if you don’t work around it).

Because WireGuard VPNs automatically let you reconnect from a different IP address, they’re also really useful if you’re using different connections. If that’s a common problem either for your IT team or for colleagues who go out on site visits, you can get laptops that come with a built-in 4G modem, and set up WireGuard so that traffic keeps flowing even when the wifi signal disappears. The same approach works for teams where you’re sometimes docked and sometimes not, and you want client connections to keep working when you move away from your desk.

The technology itself doesn’t really mind how you’re using it. Some people use WireGuard for container networking; for example, within a Kubernetes cluster. That might be useful if all or part of the cluster is running on-premises.

WireGuard’s flexibility and low overhead even lend it to some unusual situations. Recently I wanted to debug issues with builds running in GitHub Actions, so I wrote a helper that lets you VPN into the Action and troubleshoot it. In my next article, I’ll explain an easy way for you to do the same thing.


I’m a consultant at The Scale Factory where we empower technology teams to deliver more on the AWS cloud, through consultancy, engineering, support, and training. If you’d like to find out how we can support you and your team to set up VPNs and other networking options, get in touch.