How to Set Up a WireGuard VPN Server on Linux
A few months back a client in Pune called me because his office NAS kept getting probed by bots. He'd port-forwarded 445 and a web UI straight to the internet so his team could grab files from home. That's how most of my WireGuard jobs start — someone exposed something they shouldn't have, and I get called in after, not before. Now it's the first thing I set up on every client VPS, before Nextcloud, before Jitsi, before anything else touches the internet. Nobody sees the office LAN except people holding a key.
Why WireGuard (and not OpenVPN)
I used to default to OpenVPN because it's what I learned first. I don't anymore. WireGuard's codebase is roughly 4,000 lines; OpenVPN's is well over 100,000. That's not a trivia point — fewer lines means a smaller attack surface and an implementation you can actually audit in a weekend. WireGuard also skips cipher negotiation entirely: Curve25519 for key exchange, ChaCha20-Poly1305 for encryption, BLAKE2s for hashing, HKDF for key derivation. No downgrade attacks, no cipher suite bugs, no config flags to get wrong.
It also lives in the kernel. Since Linux 5.6 (2020), the module ships with the kernel itself, so there's no user-space daemon shuffling packets around. On the boxes I run — mostly Hetzner and Contabo VPS, plus a few AWS Mumbai instances for Indian clients — I've seen WireGuard push close to line rate on modern hardware; independent benchmarks put it around 9.5 Gbps versus roughly 1 Gbps for OpenVPN on the same gear. Latency overhead is under a millisecond, typically 0.3–0.8ms. CPU usage sits in the single digits where OpenVPN was chewing 20–40% on the same tunnel. On a ₹400/month Contabo box, that difference is the whole ballgame.
Before you start
You need a Linux VPS with a public IP and a kernel 5.6 or newer — any current Ubuntu, Debian, or Rocky install qualifies. Pick a private subnet for your tunnel that won't collide with anything else you run; I default to 10.8.0.0/24 across most client deployments unless they already have something on that range. And you need UDP port 51820 open on whatever firewall sits in front — cloud security group, ufw, or ISP router.
Install WireGuard and generate keys
On Ubuntu or Debian this is a two-minute job — the kernel module is already there, you just need the tools:
sudo apt update && sudo apt install wireguardThat pulls in wireguard-tools, which gives you the wg and wg-quick commands. Now generate the server's keypair. Set umask 077 first so the private key never lands on disk world-readable — I've seen boxes where a lazy deploy script skipped this and left the private key at 644.
umask 077
wg genkey | tee privatekey | wg pubkey > publickeyDo this once for the server, then once more per client. Every device gets its own keypair — I never reuse a client key across a phone and a laptop, even for the same person, because revoking one device without touching the rest is the whole point.
Server configuration
Create /etc/wireguard/wg0.conf:
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <server-private-key>
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey = <client-public-key>
AllowedIPs = 10.8.0.2/32Swap eth0 for whatever your VPS calls its public interface — check with ip route if you're not sure. Enable IP forwarding, since the kernel won't route between the tunnel and the internet without it:
echo "net.ipv4.ip_forward=1" | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --systemOpen the port on ufw if that's what you're running:
sudo ufw allow 51820/udpThen bring it up and make it survive reboots:
sudo systemctl enable --now wg-quick@wg0Client configuration
Each client gets its own .conf file:
[Interface]
Address = 10.8.0.2/32
PrivateKey = <client-private-key>
DNS = 10.8.0.1
[Peer]
PublicKey = <server-public-key>
Endpoint = your-server-ip:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25AllowedIPs = 0.0.0.0/0, ::/0 routes everything through the tunnel — full tunnel, good for someone on a coffee shop Wi-Fi who wants all their traffic protected. For the office-NAS use case, I usually go split tunnel instead: AllowedIPs = 10.8.0.0/24, so only office traffic goes over WireGuard and everything else — Netflix, YouTube, whatever — stays on the client's own connection. Keeps latency sane and doesn't route the client's entire internet life through a ₹400 VPS. PersistentKeepalive = 25 matters a lot on mobile — without it, NAT tables on Jio and Airtel routers time out the UDP mapping within a minute or two and the tunnel silently stops receiving.
For non-technical clients I don't send config files at all — I generate the QR code and have them scan it in the official app:
qrencode -t ansiutf8 < client.confThirty seconds, and it works with the official Android and iOS apps. Windows and macOS have their own official clients too — Windows has used a kernel driver since August 2021, so it's not the sluggish TAP-adapter experience OpenVPN users remember.
Test and verify
Check the tunnel on the server:
sudo wg showLook at the "latest handshake" line — if it's recent, the tunnel is live. From the client, ping the server's tunnel IP and confirm your public IP actually changed:
ping 10.8.0.1
curl ifconfig.meTo add a peer without restarting the interface — useful when a client's team grows and you don't want to bounce everyone else's connection — pipe a new peer block straight in:
sudo wg addconf wg0 /dev/stdinCommon problems
- No handshake at all — almost always a firewall or NAT blocking UDP 51820. Some corporate networks and a surprising number of hotel Wi-Fi setups drop UDP outright. If that's a recurring issue for a client, look at a TCP fallback wrapper like wstunnel or udp2raw, or just move the listen port to something like 443.
- Tunnel connects but pages hang — classic MTU problem, common on PPPoE links and cellular carriers. Drop the MTU to between 1280 and 1360 in the interface config and retest.
- DNS leaking on full tunnel — if you didn't set
DNS =in the client config, the OS falls back to its normal resolver outside the tunnel. Point it at your own resolver or the server's. - Phone battery drain — an always-on full-tunnel VPN on mobile does cost you battery. For clients who just need occasional access to office resources, split tunnel plus manual connect beats leaving it on 24/7.
WireGuard alone or a managed mesh?
Plain WireGuard has no user management and no auth beyond possessing a key — fine for one person or a small, static set of devices you control yourself. Once a client has a team of eight people who need to be added and removed regularly, hand-editing wg0.conf gets old fast. That's when I reach for Headscale (self-hosted, open source) or Tailscale (hosted) on top of WireGuard, or Firezone if there's a compliance angle.
| Option | Setup time | User management | Control plane | Best for |
|---|---|---|---|---|
| Manual WireGuard | 15–30 min | None — edit configs by hand | Self-hosted, you own it entirely | Solo admin, a handful of static peers |
| Headscale | ~1 hour | Basic ACLs, self-hosted | Self-hosted, open source | Teams who want control without a SaaS bill |
| Tailscale | ~10 min | SSO, device approval, ACLs | Hosted by Tailscale | Teams that want it to just work, no ops overhead |
Worth saying plainly: WireGuard isn't an anonymizer. Your VPS provider still sees every packet that passes through it, same as with any VPN you run yourself. And it doesn't try to hide that it's WireGuard — deep packet inspection can fingerprint the protocol, so on genuinely hostile networks it's not your tool for evading blocks.
When I'd actually use this
For a solo developer, a small office, or as the access layer in front of a Nextcloud box, manual WireGuard is the right call — it's fast to set up, fast on the wire, and there's very little to break. The moment you're managing access for a team that changes every month, layer Headscale or Tailscale on top rather than hand-editing peer lists at 11pm. I've set this up on well over thirty client servers now, and it's the one piece of infrastructure I've never had a client complain about afterward. That's rare enough to mean something.
Configure WireGuard on Linux: Debian, Ubuntu, Fedora and Alpine
The WireGuard module has been in the Linux kernel since version 5.6, so every current distro only needs the userland tools. The install commands and firewall steps differ by distro; the wg0.conf from earlier in this guide stays the same.
Debian and Ubuntu: apt and ufw
On Debian 13 (Trixie) and Ubuntu 24.04 it is one package and one firewall rule:
sudo apt update && sudo apt install wireguard
sudo ufw allow 51820/udpFedora: dnf, firewalld and masquerade
Fedora already ships the module, so you only install the tools:
sudo dnf install wireguard-toolsThe step people miss is the firewall. Fedora uses firewalld, which drops UDP 51820 by default. Open the port, turn on masquerading, then reload:
sudo firewall-cmd --permanent --add-port=51820/udp
sudo firewall-cmd --permanent --add-masquerade
sudo firewall-cmd --reloadOn a Fedora client, NetworkManager can import the config directly: nmcli connection import type wireguard file wg0.conf.
Alpine Linux WireGuard server: apk and OpenRC
Alpine has no systemd, so there is no wg-quick@wg0. Since Alpine 3.20, the wireguard-tools-openrc package provides an OpenRC service instead:
apk add wireguard-tools iptables wireguard-tools-openrc
ln -s /etc/init.d/wg-quick /etc/init.d/wg-quick.wg0
rc-update add wg-quick.wg0
rc-service wg-quick.wg0 startFor forwarding, put the sysctl file in /etc/sysctl.d/, run rc-update add sysctl, and set IPFORWARD="yes" in /etc/conf.d/iptables.
Two catches. On diskless or RAM-disk installs (modloop), the module has to be added inside the modloop image. In an LXC or Docker container, the module must already exist on the host.
WireGuard split tunneling on Linux
The client's AllowedIPs decides all routing. 0.0.0.0/0 sends everything through the tunnel. 10.8.0.0/24 sends only the server subnet.
I use the narrow version when someone only needs to reach the LAN or a NAS. Streaming and downloads stay on their own connection, battery drain is lower, and none of that traffic passes through the VPS. If subnets and routes are new to you, my Docker networking guide covers the same routing basics.
To check a split tunnel, ip route get 1.1.1.1 should name your normal gateway, not wg0. curl ifconfig.me should show your normal public IP.
Excluding one subnet from a full tunnel
A common case is a full tunnel that should still keep one network local, such as a home LAN on 192.168.1.0/24. A normal ip route entry is ignored here, because wg-quick marks packets with an fwmark and adds a policy rule pointing at its own routing table.
The fix is a higher-priority rule that sends that subnet back to the main table. Add these lines to the client config:
PostUp = ip rule add to 192.168.1.0/24 table main priority 100
PreDown = ip rule del to 192.168.1.0/24 table mainMaking WireGuard work on blocked networks with udp2raw
Some corporate, hotel and ISP networks drop UDP completely, so the WireGuard handshake never completes. udp2raw wraps the UDP packets in fake-TCP, ICMP or UDP at the raw-socket layer, so they get through as traffic the network allows.
On the VPS:
./udp2raw_amd64 -s -l0.0.0.0:4096 -r 127.0.0.1:51820 -k "long-random-password" --raw-mode faketcp -aOn the client:
./udp2raw_amd64 -c -l0.0.0.0:3333 -r <server-ip>:4096 -k "long-random-password" --raw-mode faketcp -aThen point the WireGuard client's Endpoint at 127.0.0.1:3333, and drop the interface MTU to about 1342 for udp2raw's packet overhead.
udp2raw settings that work with WireGuard
- Match both sides.
--raw-mode,-kand the auth mode must be the same on the server and the client. - Pick the mode for the network. faketcp is the default. Try
icmpif TCP is blocked too, andudpif the network only shapes UDP. - Always pass
-a. It adds the iptables rule that stops the kernel answering the fake TCP packets with RST. Forgetting it is the classic mistake, and you get a slow, unstable tunnel instead of a clear error. - Run it as root or with cap_net_raw, because it needs raw sockets.
- Check the client platform. It runs on Linux, Android, OpenWRT and Raspberry Pi. There is no iOS client, so an iPhone on a blocked network needs a different wrapper, such as wstunnel over WebSocket/TLS.
udp2raw has real costs: one more process to keep running, extra latency, and lower throughput. Use it for this one problem only, never on networks that already pass UDP. If running your own services is new to you, get plain WireGuard working first — my beginner's guide to self-hosting covers the basics.
WireGuard split tunneling and blocked networks: where the full walkthroughs are
The two sections above give you the short version of split tunneling and udp2raw. The companion guide has the full config files from start to finish. Use it when the short version gets you close but not all the way.
- WireGuard split tunneling and blocked networks holds the full udp2raw and AmneziaWG configs.
- If the point of a narrow tunnel is keeping devices on their own network segment, isolating IoT devices on your network covers the subnet side.
The MTU check that explains a tunnel that connects but stalls
The handshake succeeds, and wg show shows a recent handshake. Small pages load fine. But a large download or a big file copy just hangs.
That is usually an MTU black hole. Large packets are too big for some link on the path, so they get dropped, and nothing tells your machine why.
Finding the real MTU with a do-not-fragment ping
On Linux, the WireGuard interface MTU defaults to 1420. That is 1500 minus 80 bytes of tunnel overhead. The ping adds its own IP and ICMP headers, so the largest ICMP payload that fits is 1392 bytes. On a bare 1500 MTU path, the ceiling would be 1472. Every header costs bytes.
Ping the server's tunnel address with a full-size payload:
ping -M do -s 1392 -c 3 10.8.0.1The -M do flag sets the do-not-fragment bit. With it set, the kernel reports the failure instead of silently fragmenting the packet, so you can see the problem instead of guessing. If that ping fails, bisect with a smaller size:
ping -M do -s 1200 -c 3 10.8.0.1If the smaller ping works and the larger one fails, the real path is smaller than WireGuard assumes. Check what MTU the interface is using now:
ip link show wg0To fix it, lower the MTU in the client config:
[Interface]
MTU = 1280Then bring the interface down and up so the new value takes effect:
wg-quick down wg0
wg-quick up wg0PPPoE links (1492) and mobile networks are the usual places where 1420 fails.
udp2raw, AmneziaWG or wstunnel: match the tool to the block
Each wrapper gets around a different kind of block, so figure out which one you are facing before you pick a tool.
| Block type | The wrapper that fits | What it costs you |
|---|---|---|
| UDP is dropped, but TCP gets through | udp2raw in faketcp mode | One more daemon on both ends, extra latency, lower throughput, and it needs raw sockets |
| UDP gets through, but deep packet inspection recognises the handshake and throttles or blocks it | AmneziaWG, a WireGuard fork that pads and randomises packet sizes and headers with junk packets while keeping the same cryptography | The fork has to run on both ends, and it adds its own obfuscation settings to the config, so you cannot just drop it onto a client you do not control |
| Only TCP 443 gets out | wstunnel, which carries the tunnel inside a normal-looking WebSocket over TLS | Needs a real TLS front end and a certificate, and throughput is usually the lowest of the three |
None of the three beats a block forever. The network owner can change the rules at any time, and they get the last word.