I spent a weekend about a year ago setting this up on a Proxmox VM on node1, now it's the thing I SSH through every day. Writing it down before I forget the ten-minute version.
TL;DR
sudo apt install wireguard qrencode
wg genkey | sudo tee /etc/wireguard/srv.key | wg pubkey | sudo tee /etc/wireguard/srv.pub
# drop wg0.conf below, then:
sudo systemctl enable --now wg-quick@wg0
sudo ufw allow 51820/udp
That's it on the server side. Below is the config and what actually burned me the first time.
The server config
# /etc/wireguard/wg0.conf
[Interface]
PrivateKey = <srv.key contents>
Address = 10.8.0.1/24
ListenPort = 51820
PostUp = iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o ens18 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o ens18 -j MASQUERADE
[Peer]
# laptop
PublicKey = <paste later>
AllowedIPs = 10.8.0.2/32
[Peer]
# phone
PublicKey = <paste later>
AllowedIPs = 10.8.0.3/32
Two things I got wrong on the first attempt:
-o ens18— that's the external interface on my Proxmox VM. On bare Ubuntu it's ofteneth0, on cloud VPS usuallyens3orenp1s0.ip -br linktells you. Get this wrong and clients connect to the tunnel fine but can't reach the internet, which is confusing because the tunnel works10.8.0.0/24— pick a range you don't use anywhere else. I already had10.0.0.0/24on the LAN and10.10.0.0/24on another VPN, so 10.8 was free. Collision here eats hours
You also need IP forwarding on. If you don't have it already:
echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-wg.conf
sudo sysctl --system
Client config
On the client machine, gen a keypair and write this:
[Interface]
PrivateKey = <client private key>
Address = 10.8.0.2/32
DNS = 1.1.1.1
[Peer]
PublicKey = <server public key>
Endpoint = node1.mydomain.tld:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
AllowedIPs = 0.0.0.0/0 pushes everything through the tunnel. For split-tunnel (only reach the internal network, normal traffic stays local), use 10.8.0.0/24, 192.168.0.0/24 or whatever your internal range is.
Paste the client's public key into the [Peer] block in the server's wg0.conf, reload:
sudo wg syncconf wg0 <(wg-quick strip wg0)
syncconf re-reads without nuking existing connections. systemctl restart kills them. Use syncconf.
Phone
This is where WireGuard actually beats everything else. Generate the phone config on the server, cat it into qrencode, scan with the WireGuard Android/iOS app:
qrencode -t ansiutf8 < /etc/wireguard/phone.conf
Five seconds. No typing a 44-char base64 key on a phone keyboard.
After the scan, shred -u phone.conf so it's not sitting around.
Firewall + SSH
After the VPN is up, move SSH behind it and stop exposing port 22:
# /etc/ssh/sshd_config
ListenAddress 10.8.0.1
sudo ufw deny 22/tcp
sudo ufw allow from 10.8.0.0/24 to any port 22
sudo systemctl reload ssh
Check from a second SSH session first — don't close the original until you've tested the new one. I've locked myself out this way. Twice.
If you haven't hardened SSH beyond this, the full checklist is at SSH hardening, and fail2ban is worth the five minutes even behind a VPN.
Two gotchas I burned time on
Handshake never happens. sudo wg show shows "latest handshake: (never)". Almost always UDP 51820 not reaching the server. My first time this was the hoster's inbound ACL, not my UFW. If ss -unlp | grep 51820 on the server shows wireguard listening, the port is open locally — it's upstream.
Tunnel up, but no internet through it. This is IP forwarding off, or the MASQUERADE rule has the wrong outbound interface. Check sysctl net.ipv4.ip_forward is 1, then sudo iptables -t nat -L POSTROUTING -v to see if packets are hitting the MASQUERADE rule. Zero packets = wrong interface name.
Why this works for me
node1 runs Proxmox, one VM runs nginx + a bunch of other stuff, and this WireGuard instance lives on a small dedicated VM. My phone, laptop, and anything else I need mobile access to all land on 10.8.0.0/24, and from there I can reach every other VM on the internal bridge. No exposing SSH, no port forwarding per service.
If you're looking for a reason to care: WireGuard is the first VPN I've run that I don't actively hate.