SSD Nodes Learn
How to do am Matt ConnorBy Matt Connor · Updated 2026-07-24

how to setup WireGuard VPN for VPS

Learn how to configure wg0.conf and IP forwarding for WireGuard. We go show you how to fix handshake failures and NAT rules on your Linux VPS server.

What you are building

A WireGuard VPN on a server you own is about forty lines of config: one key pair, one interface file, one sysctl, one NAT rule, one firewall hole. The install is trivial, so most of this guide covers what breaks — key permissions, AllowedIPs, forwarding and DNS.

WireGuard is a Layer 3 tunnel in the kernel, mainline since Linux 5.6, so Ubuntu 24.04 and Debian 13 ship it with no external module. No cipher negotiation, no certificate authority, no username/password step: a peer is a public key plus the IP addresses that key may use. A packet failing its MAC check is dropped with no reply, so the port does not answer scans. The flip side: no auth server exists, so removing access means deleting a peer on the box.

Check the virtualisation first

WireGuard need kernel wey you fit load module inside, and for KVM VPS, e dey work out of the box. For container virtualisation wey dey share host kernel — OpenVZ, LXC — the first command go fail with RTNETLINK answers: Operation not supported, and the fallback na the wireguard-go userspace implementation. Check with sudo modprobe wireguard && echo ok first.

Generate keys without leaking dem

If anybody fit read your /etc/wireguard/server.key, e be like say you no use VPN at all. That common umask 077 && wg genkey | sudo tee ... line no dey work well, because sudo dey use its own umask for the file wey tee dey create. You must set the mode with your hand.

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.key

Generate the client pair for the same way. wg genpsk dey add optional pre-shared key, na 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/32

chmod 600 it; if you see startup warning say file open to everybody, e mean say you skip am. Address na the server address inside the tunnel, wey carry mask for the whole VPN subnet. Pick range wey no go clash with anything for real world — 192.168.1.0/24 go clash with half of the home routers wey your clients dey use, and the tunnel go just fail because local route take over.

Peer AllowedIPs for server side na /32, na that one tunnel address wey client get. If you give two peers the same allowed IP, e go move go whichever one you configure last, and the first one go stop to receive traffic without any error show. Leave SaveConfig unset, or wg-quick down go rewrite this file from live state.

Turn the box into a router

Linux server dey drop packets wey no be for am. Forwarding and source NAT no dey work by default.

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_forward

Bare sysctl -w go work until next reboot, then e go stop to work quietly. NAT need the egress interface — the NIC wey dey reach internet, no be wg0. No assume eth0; check your own from ip route show default, because current images dey use names like enp1s0 or ens3.

Firewall: the port, and the forward path

One nftables file dey handle filter and NAT. Write /etc/nftables.conf — e go flush all the old ruleset, so no do this one for any box wey ufw or Docker dey manage already.

#!/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
  }
}

Use sudo systemctl enable --now nftables apply am, make you keep one second SSH session open: policy drop plus one typo for the SSH rule fit lock you out from your own server. Watch wetin the forward chain no dey allow — wg0 to wg0. Peers go reach internet, but dem no go reach each other; add iifname "wg0" oifname "wg0" accept if you want peer-to-peer VPN. Na that same chain dey control wetin peer fit touch for the server itself, and dis one important when the box dey serve as remote development box wey dey run Claude Code for tmux and you no want make dem see that side publicly.

For ufw box: ufw allow 51820/udp, DEFAULT_FORWARD_POLICY="ACCEPT" for /etc/default/ufw, and one *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 show

wg-quick dey create the interface, add the addresses, and install routes wey come from AllowedIPs. enable --now na the important part: if you run wg-quick up wg0 manually, e go vanish after the next reboot, and kernel upgrades dey force reboot.

The client config, and the setting everybody gets 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 = 25

AllowedIPs dey do two different jobs at once, and when person mix dem up, na so most WireGuard confusion dey start.

For outbound, e be like routing table. If packet destination match peer AllowedIPs, the system go encrypt am and send am to that peer. 0.0.0.0/0, ::/0 dey send everything down the tunnel — full tunnel, meaning the server na default route. Split tunnel na smaller list: AllowedIPs = 10.8.0.0/24, 10.20.0.0/16 carry VPN traffic plus one private network for behind the server, and everything else go follow local route. Na that small list dey help you keep services far from public internet — like private Nextcloud instance on a VPS wey dey bound to the tunnel address, or the nested-virtualisation lab VMs wey dey run for the same box, dem go still dey reachable to peers and invisible to everybody else.

For inbound, e be like access-control list. If decrypted packet from peer source address no dey inside that peer AllowedIPs, the system go drop am. Na why the server dey list 10.8.0.2/32 for the laptop: if you put 0.0.0.0/0 for there, that client go fit spoof any address for inside the tunnel.

PersistentKeepalive na for clients wey dey behind NAT, where router dey hold the UDP mapping open only as long as packet dey flow. Once e expire, the server no go fit reach the client again. PersistentKeepalive = 25 dey hold the mapping open — set am for client side, no set am for server wey get public IP.

DNS, and the leak nobody notices

If you use AllowedIPs = 0.0.0.0/0 and you no put DNS = line, the client go still use the resolver wey e learn from local network — like the café router for 192.168.1.1. That route more specific than the default route, so DNS queries go leave through the local link for cleartext even when everything else dey inside tunnel. The traffic dey private; but the list of names no dey private.

Two honest options dey. You fit point DNS to public resolver (DNS = 9.9.9.9) and the queries go enter inside tunnel and exit from your server, even though that resolver still go see dem. Or you fit run unbound or dnsmasq bound to 10.8.0.1, set DNS = 10.8.0.1, and add udp dport 53 iifname "wg0" accept to the input chain — if you set that line and forget the resolver, 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 systemd-resolved client.

How to add and remove peers without drop the tunnel

If you restart the interface to add one user, e go disconnect everybody wey dey connect. Append the [Peer] block to wg0.conf, then reload the peer set for where e dey.

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 change while live sessions still dey work. E go update peers only: if you change Address, you still need to do full down/up. To revoke, use sudo wg set wg0 peer <public key> remove, then delete the block from the file or e go come back when you reload again.

Failure modes, with the strings you will see

Handshake never completes. wg show lists the peer with no latest handshake, and the client logs:

Handshake for peer 1 (10.0.0.10:51820) did not complete after 5 seconds, retrying (try 2)

Nothing is arriving, or nothing is accepted. In order: is UDP 51820 open on the VPS firewall and on your provider's network firewall, a separate control on most panels; is the Endpoint address and port right; are the keys crossed. The key in the client's [Peer] block must be the server's public key, and vice versa — pasting a private key, or a client's own public key, gives exactly this symptom. sudo tcpdump -ni any udp port 51820 on the server shows whether packets arrive at all. The kernel module logs nothing by default; WireGuard messages appear in dmesg only after you enable dynamic debug (echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control), and with that on, a key mismatch shows up as an invalid-MAC drop.

Handshake works, no internet. ping 10.8.0.1 succeeds but ping 1.1.1.1 times out: forwarding or NAT is missing. Check sysctl net.ipv4.ip_forward reads 1, then watch the counters while the client pings, with sudo nft list ruleset or sudo iptables -t nat -L POSTROUTING -n -v. Zero packets on the masquerade rule means its egress interface name is wrong; a rising counter with no replies points at the forward chain policy.

Internet works, names do not. ping 1.1.1.1 succeeds and curl https://example.com returns Could not resolve host. The DNS line is missing, or it names a resolver unreachable from inside the tunnel.

Some HTTPS sites hang. SSH and ping are fine; large pages stall. That is path MTU: the tunnel adds overhead, and some link in the middle drops the oversized packets without an ICMP message getting back. Lower MTU on the client [Interface] — try 1420, then 1380, then 1280.

Interface refuses to start. Address already in use means another process holds UDP 51820. Cannot find device wg0 after a failed up usually means the config was rejected; read journalctl -u wg-quick@wg0 -n 50.

Migrating from Streisand or OpenVPN

Streisand no get maintenance again and dem don archive di repository. To run VPN for abandoned automation na slow security problem. No dey upgrade am in-place, and OpenVPN PKI no fit convert: WireGuard no get certificates, no CA, and no expiry, so every client go get fresh key pair.

Migrate dem for parallel — WireGuard for UDP 51820 fit work together with OpenVPN for 1194 for di same box. Set up wg0, move clients one by one, then stop di old service. OpenVPN username/password and revocation model no dey carry over; if you need accounts or audit trail, you go need to layer dat one for top WireGuard.

Backups, upgrades, and what strains at scale

/etc/wireguard na the server. Back am up (sudo tar czf wg-backup.tgz -C /etc wireguard, mode 600, keep am far from the box) so you fit rebuild am for new VPS inside minutes. If you lose the server private key, you must reissue every client config, because clients pin the server public key. Upgrades na normal apt upgrade plus reboot for kernel updates, and wg-quick@wg0 go restart itself if you enable am.

The state for every peer small and the crypto dey run inside kernel. So, the limit na your VPS CPU and bandwidth allowance, no be anything for this config — use iperf3 for inside the tunnel to measure am instead of to trust wetin dem publish. Wetin go cause wahala when scale big na the operations. Every peer need unique tunnel IP, and if you hand-edit sixty [Peer] blocks, duplicate AllowedIPs fit sneak in: use script to generate the configs. One server na one UDP endpoint and one point of failure, and WireGuard no get clustering: redundancy mean say you need second server with its own keys. Key rotation still manual, so write down who get which key and how you go revoke one.

All of these things need Linux box wey you control — public IP, kernel wey you fit load module into, and firewall wey you control from start to end.

FAQ

Why the WireGuard handshake no dey finish?

If wg show show peer wey no get latest handshake, e mean say packets no dey reach or dem no dey accept dem. Check UDP 51820 for both VPS firewall and your provider network firewall. Make sure say Endpoint host and port correct, and check say you no mix up the keys — the client [Peer] block must get the server public key. sudo tcpdump -ni any udp port 51820 for server go show if packets dey reach at all; dmesg go only show WireGuard handshake failure after you enable dynamic debug (echo module wireguard +p | sudo tee /sys/kernel/debug/dynamic_debug/control), and when that happen, key mismatch go show as invalid-MAC drop.

The tunnel connect but I no get internet. Wetin dey miss?

If ping 10.8.0.1 dey work but ping 1.1.1.1 dey timeout, the problem na forwarding or NAT. Confirm say sysctl net.ipv4.ip_forward dey read 1 and say e don set for /etc/sysctl.d/, no be just with sysctl -w wey go die when you reboot. Then check the masquerade rule for see your real egress interface from ip route show defaultenp1s0 or ens3, or sometimes eth0.

I need the DNS = line for my client config?

If you use full tunnel and you no put DNS = line, the client go still use the resolver wey e get from local network. Those queries go dey travel for cleartext for local link even though everything else dey pass through tunnel. Point DNS to public resolver, or run unbound/dnsmasq wey bound to 10.8.0.1 and open udp dport 53 iifname "wg0" for input chain.

Wetin AllowedIPs dey control exactly?

E get two jobs. For outbound, e be routing table: any traffic wey match peer AllowedIPs go get encryption and e go go send to that peer. For inbound, e be access-control list: if decrypted packet source no be inside that peer AllowedIPs, the system go drop am. Na why server side dey list /32 for every client, while client side fit only list 0.0.0.0/0.

WireGuard go work for any VPS?

For KVM VPS, e go work with the in-kernel module without extra setup. For container virtualisation wey dey share host kernel, like OpenVZ or LXC, modprobe wireguard go fail with Operation not supported and the fallback na the wireguard-go userspace implementation. Run sudo modprobe wireguard && echo ok before you do anything else.

#wireguard#vpn#linux-networking#nftables#systemd#self-hosting