Setting up WireGuard on OpenBSD and Linux

I wanted to setup a WireGuard tunnel between my OpenBSD.amsterdam VM and one of the Raspberry Pi’s at home. Here’s what I did.

What is WireGuard?

First off, if you don’t know what WireGuard is here’s its homepage. WireGuard’s this new VPN tunnel protocol that’s way easier to setup than OpenVPN or IPsec and also more performant. It’s quite simple actually, which also means it has less features than what you might be used to with (say) OpenVPN. While you could always setup WireGuard in Linux, it was added to the kernel in kernel version 5.8. Similarly while you could use the userspace implementation of WireGuard written in Go (known as wireguard-go) it was added to OpenBSD too earlier this year. My Pi is running kernel 5.9, and I am on the latest OpenBSD, so I have nothing to install on either end (except wireguard-tools on the Pi).

The wg(4) manpage on OpenBSD is a great starting point and everything I learnt is from there plus a bit of Googling. Similarly the WireGuard website is a good starting point for the Linux side.

The thing to remember with WireGuard is that it’s sort of like SSH in terms of the keys. The same way with SSH you have private and public keys, and you keep the private key secret and add the public keys of all the clients that you’d like to allow access to your server in ~/.authorized_keys you do something similar in WireGuard wherein you keep the private key secret and add each of your tunnel endpoints as peers with their public key. When you add a peer you also specify the IP addresses you expect to hear from (or send to) on that peer and that’s how WireGuard ensures traffic from/ to a particular peer is only accepted from the IPs that are authorized to use that key. A better overview can be found on this conceptual overview page, but what I wrote above is the gist of the matter.

So let’s get to it.

Setting it up

Let’s setup OpenBSD first. I think of it as my “server” as that’s the one with a public IP and so my Pi at home will connect to it; but it doesn’t really matter. As far as WireGuard is concerned both are peers in a tunnel and if the initial IPs that are defined for either peer changes WireGuard will automatically update itself with the new IP. (That is to say if for whatever reason my OpenBSD.amsterdam VM public IP changes WireGuard won’t drop the connection; I will of course have to take a note of the new IP and add to the config for whenever the tunnel is destroyed and setup again, but while the tunnel is up and running any IP changes don’t matter).

On OpenBSD you use the ifconfig commands to create the WireGuard interface. Again, that’s one of the nice things about OpenBSD. They try and incorporate things into the standard way of things and rather than make you remember a new wg command like on Linux everything is part of the ifconfig command.

Like I mentioned earlier WireGuard needs a private key and a public key. These are base64 encoded keys and on OpenBSD we’ll make use of the openssl rand command to generate it. Here’s the command to create a key and save it to a file (OpenBSD doesn’t need you to pass the key via a file, but I save it to a file as I will need it later):

I don’t need to run this command separately; rather I can run it as part of creating the interface itself. Here’s what I am going to do to create an WireGuard interface called wg0 (they are all of for form wgN), listening on port 3000, and with the key I created using openssl rand. Note: You can use a different port – 3000 is just what I chose.

Now if I do doas ifconfig wg0 I can see the interface and also its public key.

Take a note of it. (As I said, the public key is public… which is why I put my actual key there. All you can do with it is add me as a peer to your WireGuard and let me connect to your network. :))

Now I’ll assign an IP address to this side of the tunnel. Again, pick an IP of your choice. It has to be a subnet not used anywhere else on your network else the routing will get confusing. I wasn’t using the 192.168.2.0/24 subnet so decided to go with that. I’ll assign 192.168.2.1 to the OpenBSD end and 192.168.2.3 to the Pi later.

Cool! Now let’s do the same on the Linux side. It’s slightly different there as I’ll be using the wg commands. Plus I won’t be defining WireGuard as listening on any port etc. as the Pi will be making the outgoing connecting and nothing’s going to connect to it per se (because this is behind my home router and I am not accepting any incoming connections; I could of course change things to accept incoming connections but that’s not what I want to do here).

First, add a new interface called wg0 (can be named anything really; the key thing is you define the type as wireguard).

Assign it an IP address:

Here’s a key thing to note about WireGuard. You have to assign the IP addresses for all your end-points. WireGuard does not do any IP assignment, nor can you use something like DHCP… so this part is up to you.

Now we need a private key using which I’ll initialize WireGuard. We have to use the wg command for it, though one could use openssl rand too. The key needs to be stored to a file though, but the file doesn’t matter later on as you’ll store it in a separate config file (as I’ll show later).

Add that key to the wg0 interface. I am not specifying any port to listen on below so WireGuard will pick something at random, but I can add something like listen-port 51820 to the end of the command below to have it listen on port 51820.

Now I can do sudo wg to see the public key and the interface:

Great! So now we have both sides of the puzzle.

Let’s add the OpenBSD “server” as a peer to the Pi first. Take a note of the OpenBSD public key and replace it with what I’ve put below. Also replace 192.168.2.1 with whatever you assigned the OpenBSD side earlier. Similarly if you chose a different port, put in that instead of 3000:

Now onto the OpenBSD side. Same stuff, just a different command. And I don’t specify any endpoint coz the Linux side doesn’t have a public IP and it doesn’t matter coz when the Linux side “client” connects to the OpenBSD “server” the latter will take a note of the public IP from which the Pi is connecting and take care of any changes in the IP too.

As before, change the key to match what’s on your peer. Also, read on the following paragraph before you actually run the above command on your side…

Notice the allowed-ips (on Linux) and wgaip (on OpenBSD)? That’s basically what tells WireGuard what IPs to receive from or send to on that particular tunnel. Say you set the allowed IP as 192.168.2.1 on the Linux side (as I did above; with 192.168.2.1 being the IP of the OpenBSD side of the tunnel) and the allowed IP as 192.168.2.3 on the OpenBSD side (as I didn’t do above, but bear with me for now; 192.168.2.3 being the IP on the Linux side of the tunnel) what this tells WireGuard is that on the OpenBSD side it will only accept traffic from/ send traffic to 192.168.2.3 (the Linux side) IP and if any traffic comes from/ is sent to a different IP like 192.168.2.45 using that tunnel it will be dropped. And ditto on the Linux side.

In my case, I want OpenBSD to accept traffic from/ send traffic to ANY IP subnets over the tunnel. That is to say if I get traffic from say 192.168.250.1 from the Linux side over the tunnel, I don’t want OpenBSD to drop it. Why? Because chances are at some point I might have other device in my home network point to the Pi to route traffic over to OpenBSD, and I don’t want that rejected. Similarly I might add a route on my OpenBSD side saying all my home subnets can be routed over the WireGuard tunnel, and so again I don’t want to restrict the tunnel on the OpenBSD side. Hence I leave it more accepting.

If you don’t want that, you can specify the peer IP instead:

Just for completeness, I’ll also add that there’s a wgendpoint subcommand (similar to wgaip) that one can use to specify the remote endpoint. Like I said above I am not doing that here, but I wanted to mention in case you the reader wanted to specify an endpoint.

Next, bring up the interface on the Linux side. No need to do anything similar on the OpenBSD side.

That’s all. Now I should be able to ping each peer IP from the other. If that has worked we have a tunnel successfully setup! :)

Some troubleshooting

If when you ping nothing happens check the status of the tunnel.

On Linux you can do sudo wg. On OpenBSD doas ifconfig wg0. You should see mention of the last/ latest handshake, as well as the packets sent and received. If you see handshakes, but the last/ latest was a while ago and nothing new seems to be happening it could be because of an issue in the config. In my case simply generating new keys on each end and redoing the config got things working.

Another thing could be that when pinging you see something like this:

This means the tunnel isn’t setup. I had this when I initially setup a tunnel between OpenBSD (server) and Linux (client), then I brought down WireGuard on the OpenBSD side and started it again. Since it started afresh and it didn’t know the IP of the peer endpoint to connect to (but it might have in a while I guess once the client attempts a connection to the server) the tunnel was broken. Once I brought down WireGuard on the Linux side and brought it back again things worked.

Lastly you might a message like this when pinging:

In this case check the allowed IPs. WireGuard is unable to find the key to use for the IP you are sending to, so that’s what the message means.

Saving this for the future

Assuming all good, now we got to save this for the future. So the next time OpenBSD or Linux restarts I still have the tunnel in place.

Let’s start with Linux. Run this command:

Your output will be something like this:

Copy this, then do something like sudo nano -w /etc/wireguard/wg0.conf (replace with any editor command of your choice) and paste the above with some additions:

I added a line with the IP address of the client, and I added a line to send keep-alives. I added the latter coz I noticed the tunnel would go to”sleep” after a while of inactivity and the official docs suggested a value of 25.

Now we have the client config saved in a file actually. To test that it works delete the previously created WireGuard interface:

Then bring up WireGuard using the wg-quick command:

The name wg0 should match the name of the config file, so replace accordingly.

Then tell Systemd to start the wg0 connection automatically, using wg-quick:

That’s all for Linux.

On the OpenBSD side things are easier. Create a file called /etc/hostname.wg0 with the following:

These are similar to the commands we passed to ifconfig during the initial setup. As usual OpenBSD keeps things pretty simple. Once this file exists OpenBSD will automatically bring up the interface during reboot.

Note: You’ll remember I saved the private key on the OpenBSD side even though I didn’t need to. This is why. I have to enter it in the config above.

Be sure to set this file read/write to root only:

If you want to test this you can destroy the WireGuard interface:

The bring up the interface the proper way:

Assuming that works you are set for the next reboot!

And that’s it. This is only the beginning of course, next step would be to add some routing as need (e.g. in my case tell the OpenBSD server it can access my home subnets via the tunnel).

Oh, another thing: firewall. If you are doing any blocks on the firewalls on either end be sure to whitelist the the wg0 interfaces so you don’t block any traffic to/ from that. After initial testing feel free to tighten things up.

Updates

09 Nov 2020: Learnt from the ever awesome Arch Linux wiki page that Systemd has support for WireGuard so you could configure it that way too.

Also, if you want to allow routing between other devices on the two sides of the tunnel, via the tunnel, don’t forget to enable packet forwarding and also tweak the firewall rules. I wanted traffic from OpenBSD be able to access the rest of my home network via the Pi so I had to enable forwarding on the Pi:

And also enable forwarding on the Pi’s firewall (both incoming and outgoing on the WireGuard interface, hence the two rules):

The easiest way to automate this is to add the following in the [Interface] section on the Pi.