Self-hosted VPN

🧑‍🚀 published on Fri Mar 20 2026 · 4 min read

Instead of relying on a commercial VPN provider, I wanted to see how far I could get by building my own on a VPS. The goal was simple: connect my laptop and phone over WireGuard, optionally route all traffic through it.

The real value came from debugging actual issues. This post goes through all of it.

Why WireGuard

Simpler than OpenVPN, kernel-based, and the mental model is clean. Each machine has its own private key, each knows the other’s public key, peers get assigned internal VPN IPs. No certificate infrastructure to deal with.

Architecture

WireGuard tunnel diagram

With full tunnel mode enabled, traffic flows: Laptop -> WireGuard tunnel -> VPS -> NAT -> Internet. The VPS becomes your gateway.

Server Setup

Install WireGuard and generate a key pair on the VPS:

apt update && apt install wireguard
mkdir ~/vpskeys && cd ~/vpskeys
wg genkey | tee privatekey | wg pubkey > publickey

Find the main network interface with ifconfig, you’ll need it for the NAT rule. Mine was enp1s0.

Create /etc/wireguard/wg0.conf:

[Interface]
PrivateKey = SERVER_PRIVATE_KEY
Address = 10.66.66.1/24
ListenPort = 51820

PostUp = iptables -t nat -A POSTROUTING -o enp1s0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -o enp1s0 -j MASQUERADE

Enable IP forwarding so the VPS actually forwards packets for clients. Add net.ipv4.ip_forward=1 to /etc/sysctl.conf, then:

sysctl -p

Bring the server up:

wg-quick up wg0
wg

TIP: wg with no arguments is the fastest way to check peer status at any point. Run it constantly.

Client Setup

Generate a fresh key pair on the client. Do not reuse the server keys, every peer needs its own:

wg genkey | tee client_privatekey | wg pubkey > client_publickey

Back on the server, add the client as a peer in wg0.conf:

[Peer]
PublicKey = CLIENT_PUBLIC_KEY
AllowedIPs = 10.66.66.2/32

Restart the server, then write the client config:

[Interface]
PrivateKey = CLIENT_PRIVATE_KEY
Address = 10.66.66.2/24

[Peer]
PublicKey = SERVER_PUBLIC_KEY
Endpoint = YOUR_SERVER_HOSTNAME_OR_IP:PORT
AllowedIPs = 10.66.66.0/24
PersistentKeepalive = 25
sudo wg-quick up client
sudo wg show

Errors I Hit

resolvconf: command not found — the client config had a DNS = ... line and wg-quick tried to call resolvconf. I just removed the DNS line temporarily. You can also apt install resolvconf if you need it.

Error opening terminal: xterm-kitty — nano failed when SSHing in from kitty. The VPS didn’t know the terminal type. export TERM=xterm before editing and it goes away.

Same keys on both sides — I accidentally used the server key pair on the client too. sudo wg show made it obvious: the client public key matched the server’s exactly. The rule is simple: [Interface] is this machine’s private key, [Peer] is the other machine’s public key.

Subnet conflict — I started with 10.10.0.0/24 and my laptop had a VMware interface on that same subnet. Two conflicting routes, packets going nowhere. Changed to 10.66.66.0/24 and it cleared up. Run ip route before picking a subnet.

Tunnel up, nothing passing — the interface came up but ping 10.66.66.1 hung. Client TX was climbing, RX stuck at zero, no handshake on either side. That means the server isn’t receiving anything. To check if traffic is even arriving:

tcpdump -ni enp1s0 udp port 51820

Run it on the server and ping from the client. If tcpdump shows nothing, the problem is outside WireGuard.

Provider NAT — that was exactly the case here. My VPS sits behind a provider NAT and needs port forwarding, same as SSH. I created a UDP forwarding rule in the provider panel, and the client endpoint has to use the external assigned port, not 51820. That was the missing piece.

Confirming the Tunnel

Once the endpoint and port forwarding are right, ping 10.66.66.1 should work and wg on the server should show:

latest handshake: a few seconds ago

Full Tunnel

By default I used split tunnel, which only routes VPN subnet traffic. To route everything through the VPS, change AllowedIPs on the client:

AllowedIPs = 0.0.0.0/0

Verify with curl ifconfig.me. It should return the VPS public IP.

Adding the Phone

Same process as the laptop. Generate a key pair, add a peer entry on the server with AllowedIPs = 10.66.66.3/32, create a config, then import it via QR code:

apt install qrencode
qrencode -t ansiutf8 < phone.conf

Scan from the WireGuard mobile app.

Persistence and Breakage

Enable both sides to survive reboots:

systemctl enable wg-quick@wg0        # server
sudo systemctl enable wg-quick@client # client

A few days in, the tunnel went silent. wg showed latest handshake: days ago. In a provider NAT setup, the usual causes are the port forwarding rule disappearing, the endpoint IP changing, or NAT state expiring. Check those first.

What I Learned

This forced me to think in terms of interfaces, routes, NAT, peer identity, and packet visibility. VPN setup is not only about writing config files. It’s about understanding where packets go when things don’t work.

Tagged: linuxautomation