Setting up a WireGuard VPN on a VPS in 20 minutes

WireGuard fits in about a hundred lines of config and saturates a gigabit link on an 8.50 CAD VPS. Here is the full build, server and clients.

Encrypted tunnel linking a client machine to a server

WireGuard has lived in the Linux kernel since 5.6. There is no complex negotiation, no certificates, no heavyweight daemon: one key pair per machine, a configuration file of about twenty lines, and a tunnel that saturates a gigabit link without pinning a whole core.

The most common use on a VPS: reaching the internet from a stable IP address, getting into a private network, or linking several machines without exposing them. The build below covers the first case; the other two are variations on it.

1. Install WireGuard

bash
sudo apt update
sudo apt install -y wireguard qrencode

qrencode will be used to enroll phones without retyping a key by hand.

2. Generate the keys

Every participant in the tunnel owns a pair. The private key stays on its machine; the public key gets distributed. The umask 077 prevents any other account on the server from reading the private key.

bash
umask 077
wg genkey | sudo tee /etc/wireguard/server.key | wg pubkey | sudo tee /etc/wireguard/server.pub

# and one pair per client
wg genkey | tee client-laptop.key | wg pubkey > client-laptop.pub

3. Enable routing

A Linux server does not forward packets by default. Turn it on for both address families, persistently.

bash
sudo tee /etc/sysctl.d/99-wireguard.conf <<'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF

sudo sysctl --system

4. Configure the server interface

Pick a private prefix for the tunnel: 10.8.0.0/24 for IPv4 and a fd42:42:42::/64 ULA for IPv6. First, find your public interface name.

bash
ip -brief addr   # often ens3, eth0 or enp1s0
bash
sudo tee /etc/wireguard/wg0.conf <<'EOF'
[Interface]
Address    = 10.8.0.1/24, fd42:42:42::1/64
ListenPort = 51820
PrivateKey = <contents of /etc/wireguard/server.key>

PostUp   = nft add table inet wg; \
           nft add chain inet wg postrouting { type nat hook postrouting priority 100 \; }; \
           nft add rule inet wg postrouting oifname "ens3" masquerade
PostDown = nft delete table inet wg

[Peer]
# laptop
PublicKey  = <contents of client-laptop.pub>
AllowedIPs = 10.8.0.2/32, fd42:42:42::2/128
EOF

sudo chmod 600 /etc/wireguard/wg0.conf

A dedicated nftables table beats scattered iptables rules: PostDown then removes the whole block at once, leaving no orphan rule behind after a tunnel restart. Adjust oifname to your real interface name.

5. Open the port and start

bash
sudo ufw allow 51820/udp
sudo ufw route allow in on wg0 out on ens3

sudo systemctl enable --now wg-quick@wg0
sudo wg show

wg show should list the interface, its listening port, and the declared peer. A peer with no latest handshake is simply a client that has not connected yet.

6. Configure a client

On the workstation, a symmetric file. AllowedIPs = 0.0.0.0/0, ::/0 sends all traffic through the tunnel; to route only a specific network, replace that line with the prefix in question.

wg0.conf
[Interface]
Address    = 10.8.0.2/32, fd42:42:42::2/128
PrivateKey = <contents of client-laptop.key>
DNS        = 9.9.9.9, 2620:fe::fe

[Peer]
PublicKey           = <contents of /etc/wireguard/server.pub>
Endpoint            = 198.51.100.42:51820
AllowedIPs          = 0.0.0.0/0, ::/0
PersistentKeepalive = 25

PersistentKeepalive keeps the association alive through home and mobile NATs. Without it the tunnel works client-to-server but not the other way round after a few minutes of idleness.

7. Enroll a phone

The WireGuard app reads a QR code containing the full configuration. Nothing to type, no key sent over a messaging app.

bash
qrencode -t ansiutf8 < client-phone.conf

Each new client is declared server-side with another [Peer] block and its own address in AllowedIPs:

bash
sudo wg set wg0 peer <client public key> allowed-ips 10.8.0.3/32,fd42:42:42::3/128
sudo wg-quick save wg0

8. Troubleshooting

  • No handshake: port 51820/UDP is not reachable. Test with nc -zvu 198.51.100.42 51820 and check UFW.
  • Handshake but no internet: NAT masquerading or IP forwarding is missing. Check sysctl net.ipv4.ip_forward and sudo nft list table inet wg.
  • Names do not resolve: the client's DNS directive is absent or points at a resolver unreachable from inside the tunnel.
  • IPv6 leak: if the client has native IPv6 and AllowedIPs does not include ::/0, part of the traffic bypasses the tunnel.

One last habit: sudo wg show wg0 latest-handshakes prints the last handshake timestamp per peer. It is the fastest signal for telling a network problem apart from a configuration problem.