Plan the addresses
Pick a private subnet that does not collide with your home or office network. 10.66.0.0/24 is unlikely to clash. The server takes 10.66.0.1; each client gets the next address. Pick a UDP port; 51820 is the convention.
A Spark instance handles a small team comfortably. WireGuard's cost is CPU for encryption, and one vCPU moves a lot of traffic with ChaCha20.
Server: install and generate keys
sudo apt install -y wireguard
# AlmaLinux / Rocky 9: sudo dnf install -y wireguard-tools
sudo -i
cd /etc/wireguard
umask 077
wg genkey | tee server.key | wg pubkey > server.pub
wg genkey | tee laptop.key | wg pubkey > laptop.pub
exit
umask 077 makes sure the private keys are readable only by root. Generating the client key on the server is convenient for a first setup; for long-lived devices, generate it on the device and only copy the public key across.
Server: /etc/wireguard/wg0.conf
[Interface]
Address = 10.66.0.1/24
ListenPort = 51820
PrivateKey = <contents of server.key>
[Peer]
# laptop
PublicKey = <contents of laptop.pub>
AllowedIPs = 10.66.0.2/32
On the server side, AllowedIPs is a routing rule and an access filter at once: packets from this peer are only accepted if their source is inside that range, and packets for that range are sent to this peer. Use a /32 per client.
Forwarding and NAT (only for full-tunnel clients)
If clients only need to reach services on the instance itself, skip this section. If they should send all internet traffic through London, the server must forward and masquerade.
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system
Add a NAT table and a forward rule to your nftables ruleset. Find your public interface name with ip -br link; it is often eth0 or ens18.
table ip nat {
chain postrouting {
type nat hook postrouting priority 100;
oifname "eth0" ip saddr 10.66.0.0/24 masquerade
}
}
# inside table inet filter, chain forward:
iifname "wg0" oifname "eth0" accept
iifname "eth0" oifname "wg0" ct state established,related accept
Also make sure udp dport 51820 accept is in the input chain, and allow the same UDP port in the portal firewall.
Start it
sudo systemctl enable --now wg-quick@wg0
sudo wg show
You should see the interface, its public key, the listening port, and one peer with no handshake yet.
Client config
[Interface]
PrivateKey = <contents of laptop.key>
Address = 10.66.0.2/32
DNS = 1.1.1.1
[Peer]
PublicKey = <contents of server.pub>
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.66.0.0/24
PersistentKeepalive = 25
This is a split tunnel: only traffic for 10.66.0.0/24 uses the VPN. For a full tunnel, set AllowedIPs = 0.0.0.0/0 and make sure the NAT section above is in place. Import the file into the WireGuard app on macOS, Windows, iOS or Android, or on Linux save it as /etc/wireguard/wg0.conf and run wg-quick up wg0. For phones, qrencode -t ansiutf8 < laptop.conf prints a QR code the app can scan.
Test
ping -c3 10.66.0.1 # from the client
sudo wg show # on the server: "latest handshake: 4 seconds ago"
curl -4 ifconfig.co # full tunnel only: should print 203.0.113.10
Now hide the private services
The point of the tunnel is usually what you stop exposing. Bind admin panels, database ports and metrics endpoints to 10.66.0.1 instead of 0.0.0.0, and restrict SSH to the tunnel:
iifname "wg0" tcp dport 22 accept
Keep the public SSH rule until you have confirmed you can log in over the tunnel, then remove it. The browser console in the portal remains your way in if the tunnel ever breaks.
Adding another peer
sudo wg set wg0 peer <new-client-pubkey> allowed-ips 10.66.0.3/32
sudo wg-quick save wg0
wg set applies immediately without dropping other peers. wg-quick save writes the live state back to the config file so it survives a reboot. Revoking a device is the reverse: wg set wg0 peer <pubkey> remove.
Pitfalls
- Swapping keys: each side's config holds its own private key and the other side's public key.
- MTU trouble on some mobile networks shows as tunnels where ping works but web pages stall. Set
MTU = 1380in the client's[Interface]. - Overlapping subnets: if your office uses
10.66.0.0/24already, routes conflict silently. Choose another range.