What is a VPN?
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’.
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.
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=
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
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.
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).
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
[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
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
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.