Skip to content
Security Advanced Tutorial

Build a Self-Hosted WireGuard VPN with Split Tunneling

Stand up WireGuard on a cheap Ubuntu VPS, split-tunnel your traffic, and manage peers with zero downtime.

Ji-ho Choi
Ji-ho Choi
Security & Cloud Editor · Aug 15, 2026 · 6 min read
Build a Self-Hosted WireGuard VPN with Split Tunneling

What you'll build / learn

You'll stand up a WireGuard VPN server on a cheap Ubuntu VPS, connect a laptop with split tunneling (only VPN-bound traffic goes through the tunnel), and add or revoke peers live without dropping anyone's connection.

Prerequisites

  • A VPS running Ubuntu 24.04 LTS with a public IPv4 address and root/sudo access. Any $4–6/month instance works — WireGuard is light enough for the smallest tier.
  • wireguard-tools 1.0.20210914-1ubuntu4 (what apt installs on 24.04; verified against the Ubuntu noble package index). WireGuard itself has been in the mainline kernel since Linux 5.6, and 24.04 ships 6.8, so there's no kernel module to build.
  • A Linux client machine for testing (macOS/Windows/mobile clients use the same config format via the official apps).
  • If your cloud provider has a network-level firewall or security group, you'll need access to open a UDP port there too.

Commands below were verified against the official WireGuard quickstart and the wg(8)/wg-quick(8) man pages, current as of August 2026.

1. Install WireGuard on the server

SSH into the VPS and install the package. The official install method on Ubuntu is plain apt:

sudo apt update
sudo apt install wireguard -y

This pulls in wireguard-tools, which provides the wg and wg-quick commands plus the wg-quick@.service systemd unit.

2. Generate keys and write the server config

WireGuard identity is just a Curve25519 keypair. Generate the server's as root, with umask 077 so the private key is never world-readable:

sudo -i
cd /etc/wireguard
umask 077
wg genkey | tee server.key | wg pubkey > server.pub

Find the name of your default network interface — you need it for the NAT rule:

ip route show default
# default via 203.0.113.1 dev eth0 proto static

Note the value after dev (here eth0; on many VPSes it's ens3 or enp1s0). Now create /etc/wireguard/wg0.conf, substituting your private key (cat server.key) and your interface name in both iptables lines:

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <contents-of-server.key>
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

The PostUp/PostDown hooks are bash snippets wg-quick runs when the interface comes up or down: the FORWARD rules let peers route through the server, and the MASQUERADE rule NATs their traffic out the public interface — needed for peer-to-peer reachability now and full-tunnel mode later.

3. Enable IP forwarding and open the firewall

The kernel drops forwarded packets by default; flip that persistently:

echo 'net.ipv4.ip_forward = 1' | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system

Open WireGuard's UDP port (and make sure SSH stays open before enabling ufw):

sudo ufw allow OpenSSH
sudo ufw allow 51820/udp
sudo ufw enable

If your provider has its own firewall (AWS security groups, DigitalOcean Cloud Firewalls, Hetzner firewall), open UDP 51820 there as well — this is the single most common reason handshakes silently fail.

4. Start the interface and enable it at boot

sudo wg-quick up wg0
sudo systemctl enable wg-quick@wg0

wg-quick up creates the interface, assigns 10.8.0.1/24, and runs the PostUp rules; the systemd unit re-runs it on reboot.

5. Add a client with split tunneling

On the client machine, install wireguard the same way, then generate its keypair:

wg genkey | tee client.key | wg pubkey > client.pub

Create /etc/wireguard/wg0.conf on the client:

[Interface]
Address = 10.8.0.2/32
PrivateKey = <contents-of-client.key>

[Peer]
PublicKey = <contents-of-server.pub>
Endpoint = <vps-public-ip>:51820
AllowedIPs = 10.8.0.0/24
PersistentKeepalive = 25

AllowedIPs is where split tunneling lives. It does double duty: it's both an inbound filter and the set of routes wg-quick installs. 10.8.0.0/24 means only VPN-subnet traffic enters the tunnel — your normal internet traffic keeps its direct path. Add more CIDRs (comma-separated) to route extra networks, such as a private range behind the server; setting 0.0.0.0/0 instead turns this into a full tunnel that sends everything through the VPS. PersistentKeepalive = 25 sends a packet every 25 seconds so NAT mappings on the client's side stay open — the value the WireGuard docs recommend for clients behind NAT.

Register the client on the server (paste the client's public key):

sudo wg set wg0 peer <contents-of-client.pub> allowed-ips 10.8.0.2/32
sudo wg-quick save wg0

Then bring the tunnel up on the client: sudo wg-quick up wg0.

6. Manage peers without downtime

wg set changes take effect immediately — no restart, no disconnecting existing peers. Each new device gets its own keypair and the next free /32:

sudo wg set wg0 peer <new-device-public-key> allowed-ips 10.8.0.3/32
sudo wg-quick save wg0

Revoking a device is one command:

sudo wg set wg0 peer <public-key-to-revoke> remove
sudo wg-quick save wg0

wg-quick save writes the live interface state back to wg0.conf so changes survive reboots. One caveat: it rewrites the file, so any comments you added by hand are lost. For phones, qrencode (sudo apt install qrencode, then qrencode -t ansiutf8 < client.conf) renders the config as a terminal QR code the official mobile apps can scan.

Verify it works

On the client, check the handshake and ping the server's VPN address:

sudo wg show
ping -c 3 10.8.0.1

Expected output — the latest handshake line is the proof the tunnel is live:

interface: wg0
  public key: xTIBA5rboUvnH4htodjb6e697QjLERt1NAB4mZqp8Dg=
  private key: (hidden)
  listening port: 51032

peer: HIgo9xNzJMWLKASShiTqIybxZ0U3wGLiUeJ1PKf8ykw=
  endpoint: 203.0.113.42:51820
  allowed ips: 10.8.0.0/24
  latest handshake: 8 seconds ago
  transfer: 892 B received, 1.32 KiB sent
  persistent keepalive: every 25 seconds

--- 10.8.0.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss

Confirm the split tunnel is actually splitting:

ip route get 10.8.0.1   # → "... dev wg0 ..." (VPN traffic uses the tunnel)
ip route get 1.1.1.1    # → "... dev wlan0 ..." (internet traffic doesn't)

Troubleshooting

No latest handshake line in wg show. The server never received the client's packets, or keys don't match. First check UDP 51820 in both ufw and your cloud provider's firewall. Then verify the keys: sudo wg show wg0 public-key on the server must exactly match the client's [Peer] PublicKey — a swapped public/private key produces this same silent failure.

/usr/bin/wg-quick: line 32: resolvconf: command not found. Happens on clients when you add a DNS = line (typical for full tunnel) — wg-quick shells out to resolvconf, which minimal Ubuntu installs lack. Fix: sudo apt install openresolv (or resolvconf). For split tunneling you usually don't want a DNS = line at all; deleting it also clears the error.

Warning: '/etc/wireguard/wg0.conf' is world accessible. Your config — which contains a private key — has loose permissions. Fix: sudo chmod 600 /etc/wireguard/wg0.conf.

Handshake succeeds but full-tunnel clients have no internet. Forwarding or NAT is broken on the server. sysctl net.ipv4.ip_forward must print 1 (if not, re-run sudo sysctl --system), and the interface name in your MASQUERADE rule must match ip route show default — a leftover eth0 on an ens3 box fails silently.

Next steps

Add a PresharedKey per peer (wg genpsk) for an extra symmetric layer on top of the Curve25519 handshake. Dual-stack the tunnel by adding a ULA range like fd00:8::1/64 to Address and AllowedIPs. If you outgrow hand-editing configs, the wg syncconf command (paired with wg-quick strip) applies config-file changes to a live interface diff-style — the building block most WireGuard management tools are built on. The wg-quick(8) and wg(8) man pages cover every option used here.

Sources & further reading

  1. WireGuard Quick Start — wireguard.com
  2. WireGuard Installation — wireguard.com
  3. wg-quick(8) man page — man7.org
  4. wg(8) man page — man7.org
  5. Ubuntu 24.04 (noble) wireguard-tools package — packages.ubuntu.com
Ji-ho Choi
Written by
Ji-ho Choi · Security & Cloud Editor

Ji-ho covers the increasingly tangled overlap between cloud architecture and security, drawing on a background as a penetration tester to keep his reporting grounded in real-world attack paths. He never lets a vendor claim go unquestioned and insists that every buzzword come with a proof of concept.

Discussion 0

Join the discussion

Sign in or create an account to comment and vote.

No comments yet

Be the first to weigh in.

Related Reading