The Lab

Setting Up a WireGuard VPN From Scratch

I run my own web server at home. It works fine when I am sitting next to it. The problem is when I am not.

I wanted a way to SSH in and edit files on my site from anywhere without exposing everything to the open internet. The answer was a VPN. Specifically WireGuard, which is supposed to be the modern way to do this.

It took longer than I expected :(

The first wall

The key generation command I used piped output into a directory that needed root, but sudo only covered the first part of the pipe. The fix was wrapping the whole thing in a single sudo bash call:

sudo bash -c "wg genkey | tee /etc/wireguard/server_private.key | wg pubkey > /etc/wireguard/server_public.key"
sudo chmod 600 /etc/wireguard/server_private.key

Simple once you know. Annoying before you do.

Getting it running

Getting WireGuard running on the server was straightforward after that. The config at /etc/wireguard/wg0.conf:

[Interface]
Address = 10.0.0.1/24
ListenPort = <port>
PrivateKey = <server_private_key>
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o wlo1 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o wlo1 -j MASQUERADE

[Peer]
PublicKey = <client_public_key>
AllowedIPs = 10.0.0.2/32

Enable and start it:

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

It came up clean. The Mac client connected. The tunnel showed active. And then nothing worked.

Pinging the server from outside the network timed out every time. I spent a while ruling things out. Firewall was inactive. Port forwarding was set up on the router. The server was listening on the right port. Packets were arriving at the network interface. They just never made it into the VPN tunnel.

Two problems stacked on top of each other

The actual problem turned out to be two things at once.

The masquerade rules were firing at nothing. Updating PostUp and PostDown to use eth2 fixed the routing.

Second, there was a key mismatch. One character difference between the public key stored on the server and the one actually derived from the client private key. A capital I and a lowercase l look identical depending on the font. They are not the same character.

You can verify a key pair matches like this:

# Derive the public key from the private key and compare
wg pubkey < /etc/wireguard/server_private.key
# Should match exactly what is in your peer's config

Fixing both of those and it connected immediately.

The result

# Check the tunnel is up and a peer is connected
sudo wg show

# interface: wg0
#   public key: <your_public_key>
#   listening port: 51820
#
# peer: <client_public_key>
#   endpoint: <your_ip>:<port>
#   allowed ips: 10.0.0.2/32
#   latest handshake: 4 seconds ago
#   transfer: 1.23 MiB received, 348 KiB sent

The whole thing is automatic on reboot now. WireGuard starts with the server. When I need to get in remotely I activate the tunnel on my Mac and SSH in through the VPN address:

It feels more solid than I expected for something I built in an evening.

Sometimes the issue is not the concept. It is one wrong character in a config file at midnight.