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
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.
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.
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.
ip -brief addr # often ens3, eth0 or enp1s0
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
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.
[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.
qrencode -t ansiutf8 < client-phone.conf
Each new client is declared server-side with another [Peer] block and
its own address in AllowedIPs:
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 51820and check UFW. -
Handshake but no internet: NAT masquerading or IP forwarding is
missing. Check
sysctl net.ipv4.ip_forwardandsudo nft list table inet wg. -
Names do not resolve: the client's
DNSdirective is absent or points at a resolver unreachable from inside the tunnel. -
IPv6 leak: if the client has native IPv6 and
AllowedIPsdoes 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.