Setting up a simple WireGuard tunnel

I've been spending a good amount of time lately learning about and experimenting with self-hosting and networking.

At some point I inevitably needed to figure out how to set up a secure way to connect to computers remotely without exposing them to the public Internet. A way to do this is by setting up a VPN tunnel between them, and for this purpose I decided to give WireGuard a try.

What I noticed right away when reading the quick start guide on the WireGuard website is that the steps for setting up the network interface for the VPN were a bit too manual. Since the server where I wanted to configure WireGuard runs Debian, I decided to instantiate a Systemd service using the wg-quick@.service template. In case you don't know what a Systemd template file is, the systemd.unit(5) man page explains it:

Unit names can be parameterized by a single argument called the "instance name". The unit is then constructed based on a "template file" which serves as the definition of multiple services or other units. A template unit must have a single "@" at the end of the unit name prefix (right before the type suffix). The name of the full unit is formed by inserting the instance name between "@" and the unit type suffix. In the unit file itself, the instance parameter may be referred to using "%i" and other specifiers.

Server configuration

First things first, we need to install the wireguard package:

# apt install wireguard

Now we need to generate a key pair for the WireGuard node, and a configuration file for the tunnel interface. Since these files store sensitive information, make sure to set proper file permissions:

# umask 077

Since the wireguard package created the /etc/wireguard directory with proper permissions for us, we can put the key files there:

# cd /etc/wireguard/
# wg genkey | tee privatekey | wg pubkey > publickey

We can now proceed to create the /etc/wireguard/wg0.conf file. Make sure that all these files are only readable by root.

The configuration file format is explained in the wg(8) man page:

The configuration file format is based on INI. There are two top level sections -- Interface and Peer. Multiple Peer sections may be specified, but only one Interface section may be specified.

And since we are going to start the interface with wg-quick, we have a few more options available. From wg-quick(8):

The configuration file adds a few extra configuration values to the format understood by wg(8) in order to configure additional attributes of an interface. It handles the values that it understands, and then it passes the remaining ones directly to wg(8) for further processing.

Let's start by setting up the Interface section:

# Server: /etc/wireguard/wg0.conf

[Interface]
PrivateKey = <server private key>
Address = 10.10.0.1/24
ListenPort = 51820

And that's it for the Interface section.

Now we need to configure the peers. Peers need to have their own key pairs, and we generate them using the same method that we used for the server by running the wg genkey and wg pubkey commands. Once we have the keys for the peers, we add a Peer section for each peer to the wg0.conf file on the server:

# Server: /etc/wireguard/wg0.conf

# [Interface]
# ...

[Peer]
PublicKey = <peer 1 public key>
AllowedIPs = 10.10.0.2/32

# [Peer]
# PublicKey = <peer 2 public key>
# AllowedIPs = 10.10.0.3/32
# ...

Once we finish editing the wg0.conf file, we are ready to start the WireGuard interface. To do this, we start and enable a Systemd service for the wg0 interface:

# systemctl start wg-quick@wg0
# systemctl enable wg-quick@wg0

When the service is running, you can check the status of the WireGuard interface and its peers by running the wg command without arguments.

Client configuration

If the client machine runs Linux and Systemd, the steps to configure the WireGuard interface should be mostly the same as what we did for the server. The wg0.conf for each peer should look similar to this:

# Client: /etc/wireguard/wg0.conf

[Interface]
PrivateKey = <client private key>
Address = 10.10.0.2/24

[Peer]
PublicKey = <server public key>
AllowedIPs =  10.10.0.1/32
Endpoint = <server endpoint>:51820

In this file, PrivateKey is the private key of the client, which corresponds to the public key set on the Peer section of the server configuration file. Note that Address is set to the same IP as the one from AllowedIPs on the server's Peer section (10.10.0.2), but using the same CIDR mask as the server interface (/24).

Note that we are not setting ListenPort for the client. This means that this port will be set randomly.

We are also configuring a single Peer section on the client where we set the values of the server that we want to connect to:

There are many other ways to configure a Linux WireGuard peer. If you have NetworkManager, it may be more convenient for some people to create a new connection using nmtui or the GUI tool provided by the desktop environment that you are using. Other operating systems have their own ways to configure a WireGuard connection.

Once you connect the client, you should be able to ping the server on 10.10.0.1, and the information displayed on the output of wg on either the client or the server should update.

WireGuard is very flexible, and you can tweak the configuration on each peer to talk to each other directly. You can also create network configurations like having a server acting as a hub where peers connect to, and then this hub will be in charge of forwarding packets between the clients or other networks, but this requires extra configuration for IP forwarding and masquerading on the server.