A personal VPN is one of those small-effort, big-payoff things. With a cheap VPS and twenty lines of config, you get an encrypted tunnel that lets you browse from a stable IP on hotel Wi-Fi, reach private services that never touch the public internet, and stitch a couple of servers together as a mini mesh.
WireGuard is the easiest way to do it. The kernel module ships with every modern Linux, the config files are short enough to print, and the cryptography is fast enough that on a small VPS you'll saturate the network long before the CPU.
This guide walks through a working setup on Ubuntu 22.04 or 24.04: server install, key generation, a wg0.conf for both ends, NAT and firewall rules, and a client config you can scan into the WireGuard mobile app as a QR code.
/etc/wireguard/wg0.conf on the server with a MASQUERADE rule51820 in UFW and start wg-quick@wg0qrencode -t ansiutf8 for the mobile appwg show and a quick tracerouteTotal time: about 10 minutes if DNS and SSH already work.
51820)If you don't have a VPS yet, anything in the entry tier works. WireGuard runs in kernel space, so even the smallest plans push tunnels at line rate.
Both are great. They solve slightly different problems.
If your goal is a clean exit node on a VPS you already pay for, plain WireGuard is hard to beat. If your goal is "every laptop, phone, and server in my life can ping each other," see our Tailscale guide instead. The two play well together. Some people run Tailscale for the mesh and a separate WireGuard tunnel as a regional exit.
A WireGuard server has to forward packets between the tunnel and the public interface. Out of the box, Linux drops them.
sudo apt update
sudo apt upgrade -y
Enable IPv4 forwarding so it survives reboots:
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
If you also want IPv6 inside the tunnel, add net.ipv6.conf.all.forwarding = 1 to the same file.
Verify:
sysctl net.ipv4.ip_forward
You should see net.ipv4.ip_forward = 1.
On Ubuntu the package is in the default repos:
sudo apt install -y wireguard wireguard-tools qrencode
qrencode is optional, but it's what we'll use later to make a scannable QR for the mobile app.
Confirm the kernel module is available:
sudo modprobe wireguard
lsmod | grep wireguard
If modprobe fails, you're probably on an unusual kernel. The userland fallback (wireguard-go) works but isn't covered here.
WireGuard uses a simple key model: each peer has a private key and a public key. The server needs to know every client's public key, and each client needs to know the server's public key. Nothing is signed by a CA.
Lock down the directory before writing keys to it:
sudo mkdir -p /etc/wireguard
sudo chmod 700 /etc/wireguard
cd /etc/wireguard
Generate the server keypair:
wg genkey | sudo tee server_private.key | wg pubkey | sudo tee server_public.key
sudo chmod 600 server_private.key
And one keypair per client. We'll create a single client called phone for now:
wg genkey | sudo tee phone_private.key | wg pubkey | sudo tee phone_public.key
sudo chmod 600 phone_private.key
Repeat for each device. Save the contents of each *_private.key and *_public.key somewhere you'll reference in the next two steps.
Find your VPS public network interface. On most clouds it's eth0, on Hetzner and a few others it's ens3 or similar:
ip -o -4 route show to default | awk '{print $5}'
Note the name. Now create /etc/wireguard/wg0.conf:
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = SERVER_PRIVATE_KEY_HERE
SaveConfig = false
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
# phone
PublicKey = PHONE_PUBLIC_KEY_HERE
AllowedIPs = 10.8.0.2/32
Replace SERVER_PRIVATE_KEY_HERE with the contents of server_private.key and PHONE_PUBLIC_KEY_HERE with the contents of phone_public.key. If your default interface isn't eth0, change both PostUp/PostDown lines.
A few notes:
10.8.0.0/24 is the VPN's private subnet. Pick anything in RFC1918 that doesn't overlap your home or office LAN.ListenPort = 51820 is the WireGuard convention. You can use any UDP port.MASQUERADE is the line that actually lets clients reach the public internet through the VPS. Forget it and tunnels still come up but no traffic flows.Lock the file down. WireGuard refuses to start if the config is world-readable:
sudo chmod 600 /etc/wireguard/wg0.conf
If you use UFW, allow the WireGuard UDP port and reload:
sudo ufw allow 51820/udp
sudo ufw allow OpenSSH
sudo ufw enable
sudo ufw status verbose
If you use a cloud provider firewall (Hetzner Cloud, AWS Security Groups, OVH IP-firewall, etc.), open UDP 51820 there too. UFW won't help if packets are dropped upstream.
Bring the interface up once to test:
sudo wg-quick up wg0
sudo wg show
You should see interface: wg0 and a [Peer] block with latest handshake blank for now (no client has connected yet).
Enable it on boot:
sudo systemctl enable wg-quick@wg0
sudo systemctl status wg-quick@wg0
The wg-quick@ template unit reads /etc/wireguard/wg0.conf and runs everything in it, including the PostUp/PostDown iptables rules.
On the client side, the config is even shorter. Create phone.conf somewhere you can copy or scan it:
[Interface]
PrivateKey = PHONE_PRIVATE_KEY_HERE
Address = 10.8.0.2/24
DNS = 1.1.1.1, 9.9.9.9
[Peer]
PublicKey = SERVER_PUBLIC_KEY_HERE
Endpoint = YOUR_VPS_PUBLIC_IP:51820
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
Replace the three placeholders with the phone's private key, the server's public key, and the VPS's public IPv4. The DNS line is optional but strongly recommended. Without it, the device keeps using its current resolver and you'll get DNS leaks.
The trade-off in plain English:
0.0.0.0/0): safer on hostile Wi-Fi, your real IP is hidden from sites you visit, but bandwidth is bottlenecked by the VPS link.For digital-nomad use, full tunnel is the right default. For "I just want to reach a private database on the VPS," split tunnel is plenty.
The official WireGuard apps for iOS and Android can scan a QR code instead of asking you to type a 44-character base64 key on a phone keyboard. Run this on the VPS where phone.conf lives:
qrencode -t ansiutf8 < phone.conf
A blocky black-and-white QR code prints right in your terminal. Open the WireGuard app on your phone, tap the plus button, choose Create from QR code, and point the camera at the screen. The whole config is imported in one shot.
For laptops and desktops, just copy phone.conf over and use the official client. On Linux you can drop it in /etc/wireguard/phone.conf and run wg-quick up phone.
Toggle the connection on. Then on the VPS:
sudo wg show
You should now see a recent latest handshake (under a minute old) and non-zero transfer: bytes for the peer.
On the client, check that traffic actually exits the VPS:
curl -4 ifconfig.io
The IP returned should be your VPS's public IP, not your local ISP. If you set DNS = in the client config, also test:
dig +short whoami.cloudflare ch txt @1.1.1.1
That returns the resolver IP that answered the query, which should now be one of your tunnel's resolvers.
The flow is the same as Step 3 and Step 4. For each new device:
wg genkey | tee laptop_private.key | wg pubkey > laptop_public.key[Peer] block to /etc/wireguard/wg0.conf with that public key and the next free address (10.8.0.3/32, 10.8.0.4/32, etc.).sudo wg syncconf wg0 <(wg-quick strip wg0)
If you find yourself adding peers a lot, see Going Further below for tools that automate this.
One nice property of this setup: anything bound to the VPS's tunnel IP (10.8.0.1) is reachable only by VPN clients. That makes a great home for tools you don't want on the public internet.
For example, run a private service on the VPS:
# Example: a database that listens only on the WireGuard interface
# (in postgresql.conf)
listen_addresses = '10.8.0.1'
Now 10.8.0.1:5432 works from any connected client and is invisible from the open web. No reverse proxy, no auth wall, just network-level isolation.
The same trick works for anything HTTP. Bind your dashboard to 10.8.0.1 and you have a private internal site without buying a second VPS or fighting with Cloudflare Tunnels.
Handshake completes but no traffic flows. Almost always one of two things: IP forwarding never got enabled (sysctl net.ipv4.ip_forward should be 1) or the MASQUERADE line is missing or pointing at the wrong interface. Check iptables -t nat -L POSTROUTING -n -v on the server and confirm you see a MASQUERADE rule with packet counters going up.
RTNETLINK answers: Operation not supported when running wg-quick up wg0. The kernel module isn't loaded or the running kernel doesn't have it. Try sudo modprobe wireguard. On unusual kernels (some VPS providers ship custom builds), install wireguard-dkms so the module compiles for your kernel.
DNS leaks. The client connects, traffic flows, but whoami.cloudflare reports your real ISP. You forgot to set DNS = in the client config, or the OS is using its own DoH resolver. On Windows and Android the WireGuard apps handle this automatically once DNS = is set. On macOS and iOS, double-check that the WireGuard tunnel is the active interface in network settings.
Mobile client won't reconnect after roaming between Wi-Fi and LTE. Add PersistentKeepalive = 25 to the client's [Peer] block (already in the example above). Without it, NAT mappings on carrier networks expire and the tunnel goes silent until you toggle it manually.
The config file is rejected with "permission denied" or "key file not readable". WireGuard refuses to read configs that are world-readable. Run sudo chmod 600 /etc/wireguard/*.conf and try again.
AllowedIPs, and you have a private overlay between continents. Useful for replicating databases or serving private services from two regions without exposing them publicly.wg-json (from the wireguard-tools source repo) outputs handshake state in a format Prometheus can scrape. A simple dashboard tells you when a peer hasn't checked in for an hour.ufw so SSH is only reachable through the tunnel, and the only open public port is 51820/udp plus whatever your apps need. That's the same end state as the Tailscale guide above, just with a different transport.That's the whole thing. One config file on each side, one UDP port, one MASQUERADE rule. WireGuard is small enough to fit in your head, which is rare for VPN software and a big part of why it's worth running yourself.
Need a VPS that's a good fit for a personal VPN exit node? Our Linux plans include unmetered traffic and IPv6 out of the box, and the kernel ships with WireGuard ready to go. See the options.