How to Set Up a WireGuard VPN Server on Linux

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 wireguard

That 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 > publickey

Do 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/32

Swap 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 --system

Open the port on ufw if that's what you're running:

sudo ufw allow 51820/udp

Then bring it up and make it survive reboots:

sudo systemctl enable --now wg-quick@wg0

Client 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 = 25

AllowedIPs = 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.conf

Thirty 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 show

Look 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.me

To 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/stdin

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

OptionSetup timeUser managementControl planeBest for
Manual WireGuard15–30 minNone — edit configs by handSelf-hosted, you own it entirelySolo admin, a handful of static peers
Headscale~1 hourBasic ACLs, self-hostedSelf-hosted, open sourceTeams who want control without a SaaS bill
Tailscale~10 minSSO, device approval, ACLsHosted by TailscaleTeams 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.