Self-Hosted WireGuard VPN on a VPS: Full Setup, DNS Leaks and Honest Limits
Step-by-step WireGuard VPN setup on a Linux VPS: keys, nftables NAT, IPv6, DNS leak prevention, kill switch, MTU fixes — plus what self-hosting can't hide.
Running your own WireGuard server on a VPS gives you a fast, modern VPN you fully control — no third-party client, no logging policy you have to trust, no shared exit IP. It also changes your threat model in ways most tutorials never mention. This guide covers a complete, production-usable setup on a Linux VPS, the leaks that break it, and the situations where self-hosting is the wrong tool.
The short version
A self-hosted WireGuard VPN on a VPS is excellent for:
- Encrypting traffic on hostile networks (hotel Wi-Fi, coworking, mobile tethering)
- Hiding your traffic contents and destinations from your ISP or local network operator
- Getting a stable, predictable exit IP in a specific country
- Building a private management network between your own servers
- Reaching services that must only be exposed to your VPN subnet
It is not a tool for anonymity. A VPS you pay for and use alone gives you a dedicated exit IP, which means every request you make from it is trivially correlated. Commercial VPNs at least mix you with thousands of other users on the same IP; your own server gives you a crowd of one. Self-hosting improves control and confidentiality, not unlinkability.
Keep that distinction in mind and the rest of the setup is straightforward.
Why WireGuard instead of OpenVPN or IPsec
WireGuard has been in the mainline Linux kernel since 5.6, so any current Debian, Ubuntu, Alma or Rocky VPS runs it natively with no DKMS modules. Practical consequences:
- Small attack surface. A few thousand lines of code versus hundreds of thousands for OpenVPN plus OpenSSL.
- Kernel-space crypto. Throughput is limited mainly by CPU and the network path, not by userspace context switches. On a modest KVM VPS you can usually saturate a 1 Gbit port.
- Stateless-ish design. Peers are identified by public key, not by session. Roaming between Wi-Fi and mobile data reconnects instantly.
- Silent by default. The server never replies to packets that don't authenticate, so port scans see nothing on UDP/51820.
The trade-offs: WireGuard has no built-in obfuscation, no username/password auth, and no dynamic IP assignment. Each client needs a key pair and a fixed tunnel address, which is fine for a handful of devices and annoying for hundreds.
Prerequisites
- A KVM VPS with root access and a public IPv4 (IPv6 optional but recommended)
- Debian 12 / Ubuntu 22.04+ or equivalent
- SSH key authentication already configured — do not build a VPN on a server you log into with a password
Container-based virtualization (OpenVZ, LXC) often cannot load kernel modules or manage its own netfilter tables, which breaks WireGuard or forces userspace fallbacks. Full-virtualization KVM avoids the problem entirely, which is one reason it matters more than raw specs when you pick a plan.
Step 1: Install and generate keys
apt update && apt install -y wireguard nftables
Generate the server key pair with a restrictive umask so the private key is never world-readable:
umask 077
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
Do the same per client. Generating client keys on the client device is better practice — the private key then never touches the server or your terminal scrollback. If you generate them centrally for convenience, delete them from the server afterwards.
Step 2: Enable forwarding
cat >/etc/sysctl.d/99-wireguard.conf <<'EOF'
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
sysctl --system
Forgetting this single step is the most common cause of "the handshake works but I have no internet".
Step 3: Server configuration
Pick tunnel subnets that won't collide with home LANs. Avoid 192.168.0.0/24 and 192.168.1.0/24.
/etc/wireguard/wg0.conf:
[Interface]
Address = 10.66.66.1/24, fd42:66:66::1/64
ListenPort = 51820
PrivateKey = <contents of /etc/wireguard/server.key>
MTU = 1420
[Peer]
# laptop
PublicKey = <laptop public key>
AllowedIPs = 10.66.66.2/32, fd42:66:66::2/128
[Peer]
# phone
PublicKey = <phone public key>
AllowedIPs = 10.66.66.3/32, fd42:66:66::3/128
On the server side, AllowedIPs is a routing and access-control list: only packets with those source addresses are accepted from that peer, and only traffic to those addresses is sent to it. Never use 0.0.0.0/0 in a server-side peer block unless you intend that peer to receive all routed traffic.
Step 4: NAT with nftables
Rather than shelling out to iptables from PostUp, keep a persistent ruleset. Replace eth0 with your real uplink (ip -br link):
table inet wgnat {
chain postrouting {
type nat hook postrouting priority srcnat; policy accept;
ip saddr 10.66.66.0/24 oifname "eth0" masquerade
ip6 saddr fd42:66:66::/64 oifname "eth0" masquerade
}
}
Load it into /etc/nftables.conf, then:
systemctl enable --now nftables
systemctl enable --now wg-quick@wg0
If your VPS comes with a routed IPv6 /64 or /48, you can skip NAT66 and route a sub-prefix to the tunnel instead — cleaner, and it gives each client a real global address. Whether that is desirable depends on your goals: globally reachable clients are convenient for administration and worse for exposure.
Step 5: Client configuration and the kill switch
A full-tunnel client config looks like this:
[Interface]
PrivateKey = <client private key>
Address = 10.66.66.2/32, fd42:66:66::2/128
DNS = 10.66.66.1
MTU = 1420
[Peer]
PublicKey = <server public key>
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = 203.0.113.10:51820
PersistentKeepalive = 25
PersistentKeepalive is only needed if the client sits behind NAT and you want inbound connectivity or a stable path; for a laptop that initiates everything, you can drop it and save a little battery.
On Linux clients, wg-quick supports the documented fwmark kill switch. Add to the [Interface] section:
PostUp = iptables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PostUp = ip6tables -I OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PreDown = iptables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
PreDown = ip6tables -D OUTPUT ! -o %i -m mark ! --mark $(wg show %i fwmark) -m addrtype ! --dst-type LOCAL -j REJECT
This rejects any non-local traffic that isn't marked as coming from the tunnel, so if wg0 goes down, packets fail instead of leaking to your ISP. The official Android and Windows clients have their own "block untunneled traffic" option; enable it.
Step 6: Stop DNS from leaking
DNS = 10.66.66.1 in the client config only helps if something is listening there. Two sane options:
Run a local resolver on the VPS. Install Unbound, bind it to the tunnel address, and restrict access:
server:
interface: 10.66.66.1
interface: fd42:66:66::1
access-control: 10.66.66.0/24 allow
access-control: fd42:66:66::/64 allow
access-control: 0.0.0.0/0 refuse
hide-identity: yes
hide-version: yes
qname-minimisation: yes
Never leave a resolver listening on the public interface — open resolvers get abused for DNS amplification and get your VPS null-routed.
Or point clients at a resolver you trust over the tunnel. Simpler, but you move the metadata to a third party.
Either way, understand what you achieved: your local network and ISP no longer see your queries. The recursive path from your VPS to authoritative servers is still visible to the datacenter's upstream, and your queries now all originate from one dedicated IP. If DNS metadata is the thing you care most about, encrypted resolvers plus a shared exit are a better fit than a personal resolver.
The classic residual leak is on Windows and macOS, where a captive-portal or secondary interface resolver can still answer first. Test after every setup change: check that your public IP, reverse DNS and resolver all report the VPS, not your ISP, and that IPv6 tests do not fall back to your home address.
Step 7: MTU, the invisible problem
WireGuard adds roughly 60 bytes of overhead over IPv4 and 80 over IPv6. With a standard 1500-byte path, 1420 works. On PPPoE lines (1492 MTU), some mobile networks, or when your endpoint is IPv6, 1420 is too high and you get a tunnel where SSH works, pings work, and large HTTPS pages hang forever — classic MTU black hole.
Find the real limit from the client, outside the tunnel:
ping -M do -s 1372 -c 3 203.0.113.10
If that fails, decrease until it succeeds. The working payload size plus 28 gives your path MTU; subtract 60 (IPv4 endpoint) or 80 (IPv6 endpoint) for the tunnel MTU. Values between 1280 and 1412 are common in the field. 1280 is always safe and slightly less efficient.
Common mistakes
Using AllowedIPs = 0.0.0.0/0 on both sides. On the server that turns every peer into a default gateway candidate and produces bizarre routing.
Reusing one key pair across devices. WireGuard identifies peers by key. Two devices sharing a key will fight over the same tunnel IP and the server's endpoint tracking will flap between them.
Blocking UDP/51820 by accident. Many provider firewalls and default nftables rulesets are input-drop. Confirm with wg show on the server: if latest handshake never populates, the packets aren't arriving.
Forgetting the server is now a legal and technical exit point. All client traffic appears to originate from your VPS IP. Abuse complaints, blocklist entries and rate limits land on you. Keep the peer list small and known.
Treating the VPN as a substitute for endpoint security. WireGuard encrypts the transport. It does nothing about browser fingerprinting, logged-in accounts, malware, or a compromised laptop.
What self-hosting actually protects — and what it doesn't
Protects against:
- Passive observation on the local network and by your access ISP
- DNS and traffic-destination visibility on untrusted Wi-Fi
- Shared-IP reputation problems from noisy commercial VPN exits
- Vendor-side logging policies you cannot verify
Does not protect against:
- Correlation. A single-tenant IP maps one-to-one to you. Any service that sees that IP over time builds a coherent profile.
- Your hosting provider and its upstreams. They see the encrypted tunnel, its volume, timing, and both endpoints — including the home or mobile IP you connect from.
- Legal process. A VPN server is a server. If it can be identified, it can be the subject of a request to the provider or datacenter, regardless of how you paid.
- Application-layer identity. Cookies, logins, fingerprints, and behavioural patterns are untouched by any VPN.
- Traffic classification. Vanilla WireGuard is recognizable to deep packet inspection. In restrictive networks it can be throttled or dropped; that requires obfuscation layers, not a bigger key.
If your goal is resisting correlation rather than protecting confidentiality on a hostile link, Tor or a well-audited multi-user VPN is the appropriate tool. Choosing between them honestly matters more than any config file, which is the same reasoning behind what full-disk encryption on a VPS can and cannot protect.
Where self-hosting is the right choice, the details of the host matter: jurisdiction, routing quality to your usual locations, and how much identifying information the signup process demands. If you'd rather your VPN endpoint not be tied to a billing identity, paying with Monero and no KYC removes the payment link — while the connection between your home IP and the server remains visible to network observers. For European latency and solid transit into Frankfurt, Vienna and Bucharest, Romania is a practical location for privacy-oriented VPS hosting, and IronBalkans provides full-root KVM instances there that can be deployed in under a minute.
One more practical note: VPN throughput depends heavily on the node's real CPU availability, since WireGuard encryption is CPU-bound. If your tunnel tops out well below the port speed, check for CPU steal time and I/O contention before blaming the config.
FAQ
Does WireGuard work on a NAT-only VPS without a dedicated IPv4? It needs a reachable UDP port. Shared-IPv4 setups with a port forward work if the provider forwards a UDP port to you; pure IPv6-only servers work only if all your clients have IPv6.
How many clients can one small VPS handle? Peer count is cheap; bandwidth and CPU are the limits. A 1 vCPU instance comfortably serves a handful of personal devices at hundreds of Mbit/s. Concurrent heavy streaming for a dozen users needs more cores.
Can I split-tunnel so only some traffic uses the VPN?
Yes — set AllowedIPs on the client to the specific prefixes you want routed, e.g. 10.0.0.0/8 for internal services. Everything else uses the local route. This is the correct approach for server-to-server management networks.
Should I change the default port to avoid scanning? It doesn't matter much for security since unauthenticated packets are ignored, but moving to a common port (e.g. 443/UDP) sometimes helps on networks that block unusual UDP ports.
Do I need fail2ban for WireGuard? No. There is nothing to brute-force. Spend that effort keeping the kernel patched and SSH key-only.
Takeaway
WireGuard on your own VPS is one of the highest-value hours a technical user can spend: fifteen lines of config give you strong transport encryption, a stable exit location, and a private network for your own infrastructure. Get forwarding, NAT, DNS, MTU and the kill switch right, verify with real leak tests, and keep the peer list small. Then be precise about what you've built — a confidentiality and control tool, not an anonymity system.
