How to Set Up WireGuard VPN for VPS
Set up WireGuard for your Linux VPS with wg0.conf, key permissions, IP forwarding, NAT, DNS, AllowedIPs, and fixes for handshake failures.
Wetin you dey build
WireGuard VPN for server wey you own dey need about forty lines of config: one key pair, one interface file, one sysctl, one NAT rule, and one firewall hole. The installation dey easy, so most of this guide dey cover wetin fit spoil, key permissions, AllowedIPs, forwarding, and DNS.
WireGuard na Layer 3 tunnel for the kernel, and e don dey mainline since Linux 5.6. So Ubuntu 24.04 and Debian 13 ship am without external module. No cipher negotiation, no certificate authority, and no username/password step: peer na public key plus the IP addresses wey that key fit use. If packet fail MAC check, system go drop am without reply, so the port no go answer scans. But because no auth server dey, to remove access you must delete the peer for the box.
Check virtualisation first
WireGuard need kernel wey you fit load module into, and e dey work out of the box for KVM VPS. For container virtualisation wey dey share host kernel, like OpenVZ and LXC, the first command go fail with RTNETLINK answers: Operation not supported. For this case, use wireguard-go userspace implementation instead. Run sudo modprobe wireguard && echo ok first to check.
Generate keys without leaking dem
If everybody fit read /etc/wireguard/server.key, e be like say VPN no dey at all. The common umask 077 && wg genkey | sudo tee ... command no reliable, because sudo dey apply its own umask to the file wey tee create. Set the mode directly.
sudo apt update && sudo apt install -y wireguard nftables
sudo install -d -m 700 /etc/wireguard
sudo sh -c 'umask 077; wg genkey > /etc/wireguard/server.key'
sudo sh -c 'wg pubkey < /etc/wireguard/server.key > /etc/wireguard/server.pub'
sudo chmod 600 /etc/wireguard/server.keyGenerate the client key pair the same way. wg genpsk go add optional pre-shared key, one line for each config.
The server interface: /etc/wireguard/wg0.conf
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <contents of /etc/wireguard/server.key>
[Peer]
PublicKey = <laptop public key>
PresharedKey = <psk, optional>
AllowedIPs = 10.8.0.2/32chmod 600 e mean say you skip am; startup warning wey talk say everybody fit read the file mean say you no protect am. Address na the server address inside the tunnel, and e carry the mask for the whole VPN subnet. Choose range wey you no go meet for normal network, because 192.168.1.0/24 dey clash with plenty home routers wey your clients dey behind, and local route go silently win over the tunnel.
A peer AllowedIPs for the server side na /32, the only tunnel address wey that client get. If you give two peers the same allowed IP, e go move to the peer wey you configure last, and the first one go stop receiving traffic without any error for anywhere. Leave SaveConfig unset, or wg-quick down go rewrite this file from the live state.
Make the box turn into router
Linux server dey drop packets wey no address am. By default, forwarding and source NAT no dey enabled.
printf 'net.ipv4.ip_forward = 1\nnet.ipv6.conf.all.forwarding = 1\n' \
| sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system
sysctl net.ipv4.ip_forwardBare sysctl -w go work until next reboot, then e go quietly stop. NAT need the egress interface, meaning the NIC wey dey reach internet, no be wg0. No assume say na eth0; collect your own from ip route show default, because current images dey use names like enp1s0 or ens3.
Firewall: port and forward path
One nftables file dey cover filter and NAT. Write /etc/nftables.conf, e flush the existing ruleset, so no run am for box wey ufw or Docker dey already manage.
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
ct state established,related accept
iif lo accept
tcp dport 22 accept
udp dport 51820 accept
}
chain forward {
type filter hook forward priority filter; policy drop;
ct state established,related accept
iifname "wg0" oifname "enp1s0" accept
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.8.0.0/24 oifname "enp1s0" masquerade
}
}Apply am with sudo systemctl enable --now nftables, and keep second SSH session open: policy drop plus typo for SSH rule fit lock you out of your own server. Notice wetin the forward chain no allow, wg0 to wg0. Peers fit reach internet, but dem no fit reach each other; add iifname "wg0" oifname "wg0" accept for peer-to-peer VPN. This same chain control wetin peer fit touch for the server itself. This matter when the box dey also work as remote development box wey dey run Claude Code for tmux, and you no want expose that side publicly.
For ufw box: ufw allow 51820/udp, DEFAULT_FORWARD_POLICY="ACCEPT" inside /etc/default/ufw, and a *nat POSTROUTING MASQUERADE rule for top of /etc/ufw/before.rules.
Bring am up under systemd
sudo systemctl enable --now wg-quick@wg0
sudo wg showwg-quick dey create the interface, add the addresses, and install routes wey e derive from AllowedIPs. enable --now na the important half: wg-quick up wg0 wey person run by hand go disappear after the next reboot, and kernel upgrades mean say system go reboot. Unit wey fail to come back after any of those reboots go remain quiet until person try connect, so OnFailure= drop-in for wg-quick@wg0 wey point to your own ntfy server na the cheapest way to hear about am for your phone instead of hearing from person wey lock out.
Client config, plus the setting wey almost everybody dey get wrong
[Interface]
PrivateKey = <laptop private key>
Address = 10.8.0.2/32
DNS = 10.8.0.1
[Peer]
PublicKey = <server public key>
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25AllowedIPs dey do two different work at once, and na mixing dem up dey cause most WireGuard confusion.
For outbound traffic, na routing table e be. If packet destination match peer AllowedIPs, e go encrypt the packet and send am go that peer. 0.0.0.0/0, ::/0 go send everything through the tunnel. This one na full tunnel, with the server as default route. Split tunnel get shorter list: AllowedIPs = 10.8.0.0/24, 10.20.0.0/16 go carry VPN traffic plus one private network wey dey behind the server, while everything else go continue to use the local route. Na this shorter list let you keep services completely off public internet. For example, a private Nextcloud instance for VPS wey bind to the tunnel address, or the nested-virtualisation lab VMs wey dey run for the same box, go remain reachable to peers but invisible to everybody else.
For inbound traffic, na access-control list e be. If decrypted packet from peer get source address wey no dey inside that peer's AllowedIPs, system go drop am. Na why server dey list 10.8.0.2/32 for the laptop: if you put 0.0.0.0/0 there, that client fit pretend say e be any address for the tunnel.
PersistentKeepalive na for clients wey dey behind NAT, where router dey keep UDP mapping open only while packets dey flow. When the mapping expire, server no fit reach the client again. PersistentKeepalive = 25 dey keep the mapping open. Set am for the client, no be for server wey get public IP.
DNS, and the leak wey nobody dey notice
With AllowedIPs = 0.0.0.0/0 and no DNS = line, the client go keep the resolver wey e learn from the local network, the café router for 192.168.1.1. That route specific pass the default route, so DNS queries go comot through the local link as cleartext while everything else dey pass through the tunnel. The traffic private; the list of names no be private.
Two honest options dey. Point DNS to a public resolver (DNS = 9.9.9.9), and the queries go pass through the tunnel and comot from your server, although that resolver still fit see dem. Or run unbound or dnsmasq wey dey bound to 10.8.0.1, set DNS = 10.8.0.1, and add udp dport 53 iifname "wg0" accept to the input chain, set that line and forget the resolver, then nothing go resolve at all.
For Linux clients, wg-quick dey apply DNS through resolvconf; if e no dey, you go get resolvconf: command not found. Install openresolv, or set PostUp = resolvectl dns %i 10.8.0.1 for a systemd-resolved client.
Peer dem fit add and remove without dropping the tunnel
If you restart the interface to add a user, everybody wey connect go disconnect. Append the [Peer] block to wg0.conf, then reload the peer set without taking the interface down.
sudo bash -c 'wg syncconf wg0 <(wg-quick strip wg0)'wg-quick strip go print the config without the wg-quick-only keys (Address, DNS, PostUp), and syncconf go apply the difference while live sessions remain active. E only updates peers: if Address change, you still need complete down/up. Revoke am with sudo wg set wg0 peer <public key> remove, then delete the block from the file; otherwise e go return for the next reload.
Failure modes, plus the strings wey you go see
Handshake no dey complete. wg show list the peer without latest handshake, and client logs show:
Handshake for peer 1 (10.0.0.10:51820) did not complete after 5 seconds, retrying (try 2)Nothing dey arrive, or nothing dey accepted. Check dem in this order: UDP 51820 open for the VPS firewall and your provider network firewall? For most panels, na separate control. Check whether the Endpoint address and port correct. Check whether the keys cross. The key for the client's [Peer] block must be the server public key. If you paste private key, or the client's own public key, you go get this exact symptom. sudo tcpdump -ni any udp port 51820 for the server show whether any packet dey arrive at all. The kernel module no dey log anything by default. WireGuard messages go appear for dmesg only after you enable dynamic debug (echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control). When debug dey on, key mismatch go show as invalid-MAC drop.
Handshake dey work, but internet no dey. ping 10.8.0.1 succeed but ping 1.1.1.1 time out: forwarding or NAT dey missing. Check whether sysctl net.ipv4.ip_forward read 1. Then monitor the counters while client dey ping, with sudo nft list ruleset or sudo iptables -t nat -L POSTROUTING -n -v. If masquerade rule get zero packets, its egress interface name wrong. If counter dey rise but no reply dey return, check the forward chain policy.
Internet dey work, but names no dey resolve. ping 1.1.1.1 succeed and curl https://example.com return Could not resolve host. The DNS line dey missing, or e name resolver wey no reachable from inside the tunnel.
Some HTTPS sites dey hang. SSH and ping dey work, but large pages dey stall. Na path MTU problem: the tunnel add overhead, and some link for the middle dey drop oversized packets without ICMP message returning. Reduce MTU for the client [Interface]. Try 1420, then 1380, then 1280. If reducing MTU stop the stalls but throughput still no good, stop guessing round numbers. Work through how to find the real path MTU by bisection and clamp TCP MSS. This one also rule out causes wey no relate to the tunnel at all.
Interface refuse to start. Address already in use mean another process dey hold UDP 51820. Cannot find device wg0 after failed up usually mean the config reject. Read journalctl -u wg-quick@wg0 -n 50.
Migrate from Streisand or OpenVPN
Streisand no dey maintained again and dem don archive the repository. To run VPN with abandoned automation na security problem wey dey grow slowly. No in-place upgrade dey, and OpenVPN PKI no fit convert. WireGuard no get certificates, CA, or expiry. So every client must get fresh key pair.
Do the migration in parallel. WireGuard for UDP 51820 fit run together with OpenVPN for 1194 on the same box. Set up wg0, move clients one by one, then stop the old service. OpenVPN username/password and revocation model no dey carry over. If you need accounts or audit trail, put that layer above WireGuard.
Backups, upgrades, and wetin dey put pressure for scale
/etc/wireguard na the server. Back am up (sudo tar czf wg-backup.tgz -C /etc wireguard, mode 600, keep am outside the box) and you fit rebuild am for fresh VPS within minutes. If you lose the server private key, you must issue every client config again, because clients pin the server public key. Upgrades na normal apt upgrade plus reboot when kernel updates happen, and wg-quick@wg0 go return by itself if you enable am.
State for each peer small, and crypto dey run inside kernel. So the limit na your VPS CPU and bandwidth allowance, not anything for this config. Measure am with iperf3 across the tunnel instead of trusting published figure. Wetin dey put pressure for scale na operations. Every peer need unique tunnel IP. If you dey edit sixty [Peer] blocks by hand, duplicate AllowedIPs fit enter easily: generate the configs with script. One server na one UDP endpoint and one point of failure. WireGuard no get clustering. Redundancy mean say you need second server with its own keys. Key rotation still manual, so write down who hold each key and how you go revoke one. When that record-keeping pass wetin text file fit handle, the usual answer na control plane on top of the same kernel data plane. self-hosted NetBird server fit handle address allocation, peer distribution, and setup keys wey you for otherwise do by hand. If running the control plane yourself mean one box too many, Tailscale fit host one for you, and its free plan cover six users with unlimited devices, enough say most personal fleets no go ever pay for am. After that point, na people dey determine the cost, not machines. So wetin household or small team really go pay depend on how many people get logins, not how many peers you for edit by hand inside wg0.conf. For that side, split-tunnel AllowedIPs bookkeeping become advertising your private ranges from a subnet router, wey you announce once from one VPS and approve centrally instead of pasting am inside every client file. Whether that trade make sense depend on wetin hosted control plane fit actually reach, and e no ever hold the keys wey encrypt your traffic, though e decide which peers learn about each other.
All this need Linux box wey you control, public IP, kernel wey you fit load module into, and firewall wey you own from end to end.
FAQ
WireGuard handshake no dey complete. Why?
wg show listing peer without latest handshake mean say packets no dey arrive or dem no dey accepted. Check UDP 51820 for both the VPS firewall and your provider separate network firewall. Confirm the Endpoint host and port. Then check say the keys no cross. The client [Peer] block must contain the server public key. sudo tcpdump -ni any udp port 51820 for the server shows whether packets dey arrive at all. dmesg only reports WireGuard handshake failures after you enable dynamic debug (echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control). After that, key mismatch go show as invalid-MAC drop.
The tunnel connect, but internet no dey work. Wetin dey miss?
ping 10.8.0.1 wey dey work while ping 1.1.1.1 dey time out points to forwarding or NAT problem. Confirm say sysctl net.ipv4.ip_forward dey read 1 and say e dey set inside /etc/sysctl.d/, not only with sysctl -w wey go stop after reboot. Then check say the masquerade rule dey name the real egress interface from ip route show default, enp1s0 or ens3. For rare cases, na eth0.
I need DNS = line for my client config?
With full tunnel and no DNS = line, the client go keep the resolver wey e learn from the local network. Those queries go leave in cleartext through the local link, while everything else dey pass through the tunnel. Point DNS to a public resolver, or run unbound/dnsmasq bound to 10.8.0.1 and open udp dport 53 iifname "wg0" for the input chain.
Wetin AllowedIPs actually control?
E get two jobs. For outbound traffic, e be routing table: traffic wey match peer AllowedIPs go encrypt and send to that peer. For inbound traffic, e be access-control list: if decrypted packet source dey outside that peer AllowedIPs, system go drop am. Na why server side dey list one /32 for each client, while client side fit list 0.0.0.0/0.
WireGuard go run for any VPS?
For KVM VPS, e dey work with the in-kernel module and no extra setup. For container virtualisation wey share host kernel, like OpenVZ or LXC, modprobe wireguard dey fail with Operation not supported. The fallback na wireguard-go userspace implementation. Run sudo modprobe wireguard && echo ok before you do anything else.