Set up your own WireGuard VPN with a Raspberry Pi at home as the exit gateway and a cheap VPS as the public-facing hub. Designed to work even when your home ISP uses CGNAT (Carrier-Grade NAT), which makes traditional "port-forward at home" setups impossible.
Tested on:
- Raspberry Pi Zero W (ARMv6) / Pi 4 / Pi 5 running Raspberry Pi OS Lite (Bookworm or Trixie)
- Any cheap Linux VPS (Ubuntu 22.04+, Debian 12+) — DigitalOcean, Hetzner, Vultr, Oracle Free, etc.
Phone / Laptop (anywhere) ──UDP──> VPS (public IP) ──UDP──> Pi at home
│
▼ MASQUERADE
Home internet
(exits with home IP)
- VPS is the meeting point. Both phone and Pi dial outbound to it; nothing needs to be reachable at home from the public internet. This bypasses CGNAT.
- Phone traffic exits via your home IP. Useful for geo-locked services that whitelist your home address, or to look like you're at home when you're not.
- Home IP can change freely. The Pi just re-dials the VPS from its new IP.
- Works from countries with VPN restrictions — the
hub uses a non-default UDP port. If DPI gets aggressive one day, you can wrap
WireGuard in
wstunnel/udp2raw/v2ray(not automated here yet).
Hardware + OS
- A Raspberry Pi at home with Raspberry Pi OS Lite flashed.
- A small Linux VPS (Ubuntu / Debian) with public IPv4, ~$4–6/mo. Pick a region outside the country you'll be travelling to.
- SSH key-based access to both. CGNAT at home is fine.
Flash the Pi headlessly
Use Raspberry Pi Imager → "Edit Settings" before writing, and pre-set: hostname, username/password, WiFi, locale, and SSH → "Allow public-key authentication only" with your ~/.ssh/id_*.pub pasted in. See the official headless config docs if anything's unclear. After first boot, ssh <user>@<hostname>.local should land you in.
Pick the Pi's static IP
pi/02-static-ip.sh sets the static IP on the Pi itself via NetworkManager — no router DHCP reservation needed. But the STATIC_IP you pick must be outside your router's DHCP pool, or DHCP can hand the same address to another device and conflict. Find the DHCP range in your router's admin page, then pick something below or above it.
Once you can SSH into the Pi, grab the rest of the .env values from it:
ip route | awk '/default/{print $3}' # LAN_GATEWAY (router IP)
ip -o link | awk -F': ' '{print $2}' # NET_IFACE (usually wlan0 or eth0)LAN_PREFIX is almost always 24.
.
├── README.md ← you are here
├── .env.example ← copy to .env on the Pi, edit values
├── lib/common.sh
├── vps/ ← run these ONCE on the VPS
│ ├── 01-install.sh ← installs WG, generates keys, writes hub config, prints Pi creds
│ ├── 02-harden.sh ← key-only SSH, sudo passwords, ufw, fail2ban — run after 01
│ ├── wg-add ← add a client (phone, laptop, ...) — generates QR + config
│ ├── wg-rm ← revoke a client
│ ├── wg-list ← list clients with handshake / transfer stats
│ ├── wg-show ← re-print an existing client's config + QR code
│ ├── wg-hub-policy-route ← re-adds the wg0→table-100 policy rule (idempotent)
│ └── wg-hub-policy-route.service ← runs it on every systemd-networkd restart
└── pi/ ← run these on the Pi
├── 01-update.sh ← apt update + full-upgrade (slow on Pi Zero)
├── 02-static-ip.sh ← static LAN IP via nmcli, reboots
└── 03-pi-as-exit.sh ← installs WG + dnsmasq, writes config with NAT/MSS-clamp, starts the tunnel
ssh root@vps
git clone https://github.com/emeraldtarek/pi-wireguard-bootstrap.git
cd pi-wireguard-bootstrap/vps
sudo ./01-install.shAt the end it prints a block of 6 values (VPS IP, port, pubkey, Pi private key,
Pi PSK, Pi VPN IP). Copy these — you'll paste them into the Pi's .env.
Then harden the host (key-only SSH, no NOPASSWD sudo, ufw, fail2ban):
sudo ./02-harden.shMake sure your SSH public key is in /root/.ssh/authorized_keys BEFORE running
this — afterwards, password SSH is disabled.
ssh pi@<pi-ip>
git clone https://github.com/emeraldtarek/pi-wireguard-bootstrap.git
cd pi-wireguard-bootstrap
cp .env.example .env
nano .env # paste the 6 VPS-side values + edit LAN settings
./pi/01-update.sh # slow on Pi Zero W (~10–20 min)
./pi/02-static-ip.sh # reboots; reconnect at the new static IP
# After the reboot, SSH back in (now at $STATIC_IP) and continue:
ssh pi@<new-static-ip>
cd pi-wireguard-bootstrap
./pi/03-pi-as-exit.shWhen pi/03 finishes, the Pi pings the VPS over the tunnel to confirm.
ssh root@vps
sudo wg-add phone # generates keys, allocates next free 10.10.0.x IP, prints QR + config
sudo wg-add laptop
sudo wg-add ipad- Mobile: scan the QR code from the terminal output with the WireGuard app.
- Desktop: copy
/etc/wireguard/<name>.conffrom the VPS, import into the WireGuard app.
sudo wg-list # show all clients, handshake age, bytes transferred
sudo wg-show phone # re-print an existing client's config + QR (e.g. to re-add the device)
sudo wg-rm laptop # revoke a device (immediate; removes peer from live WG)- Pi can't reach the VPS: check VPS firewall (
sudo ufw status/sudo iptables -L INPUT) and confirm UDPWG_PORT(default41194) is open inbound. - Phone shows VPN ON but no internet: most often the Pi tunnel is down. Check
ssh pi sudo wg show— look for a recent handshake with the VPS. PersistentKeepalive=25 should keep it alive; if not, the Pi may have lost WiFi. - Phone exits with VPS's IP instead of home IP (or no internet at all): VPS policy
routing isn't sending forwarded client traffic to the Pi. Check
ssh vps "sudo ip rule"— there should be afrom all iif wg0 lookup 100line. That rule is installed by01-install.shvia the[Interface]PostUpand applies to every client automatically. If missing, restartwg-quick@wg0(or justsudo ip rule add iif wg0 lookup 100). Note: asystemd-networkdrestart (e.g. an unattended systemd upgrade) silently flushes this rule while the tunnel stays up — the VPN keeps handshaking but all clients lose internet.wg-hub-policy-route.service(installed by01-install.sh) guards against this by re-adding the rule on every networkd restart; confirm it's active withssh vps "systemctl status wg-hub-policy-route". - Pi shows handshakes sent but
0 B received: the PSK on the Pi's.envdoesn't match the VPS's pi peer. Re-copyPI_PSKfromvps/01-install.sh's banner output, thensudo wg-quick down wg0 && sudo wg-quick up wg0on the Pi. - Slow throughput: Pi Zero W maxes out around 20–40 Mbps due to ARMv6 + WiFi. Use a Pi 4 / 5 with ethernet if you need more.
- Clients have no DNS / can't resolve names: client configs from
wg-addpointDNSat the Pi's tunnel IP (10.10.0.2), wherednsmasqanswers and forwards to your home router. Confirm it's up withssh pi "systemctl status dnsmasq"and that it's listening withssh pi "sudo ss -lunp | grep ':53'". By default it forwards toLAN_GATEWAY; setCLIENT_DNS_UPSTREAMin the Pi's.envand re-runpi/03-pi-as-exit.shto point it elsewhere. Clients created before this change still use public resolvers — re-runwg-add(orwg-show) to regenerate them, and update theDNSfield on any already-imported device to the Pi's tunnel IP. If your exit Pi runs Pi-hole, don't let this install a seconddnsmasq(they clash on port 53) — point clients at the Pi and let Pi-hole answer instead.
This setup already exits from your home residential IP, which sidesteps the most common VPN check (datacenter-IP reputation lists). Two further tweaks reduce fingerprinting:
- MSS clamping + a pinned 1380 MTU (VPS and Pi
wg0, plus client configs) keep the negotiated TCP MSS consistent and stop large segments from black-holing across the phone→VPS→Pi double hop. A stalled, retransmit-heavy connection is both a usability bug and a behavioral tell. - DNS through the Pi (
dnsmasq→ home router) means client lookups egress your home network exactly like any other device, instead of hitting1.1.1.1/8.8.8.8from a residential IP and letting CDNs geo-steer somewhere inconsistent with the exit.
What this can't hide: the extra latency of the double hop. An app that measures round-trip time and compares it against the exit IP's expected geolocation (RTT triangulation) can still infer a tunnel — that's inherent to bouncing through the VPS to beat CGNAT. Mobile apps may also compare the device's SIM/carrier country, GPS, timezone and locale against the IP; align those on the device if an app keys on them.
- The
.envfile contains the Pi's WireGuard private key and PSK. Keep it readable only by the user who runs the scripts..gitignoreis set to never commit it. wg-addwrites client configs to/etc/wireguard/<name>.confon the VPS. These contain client private keys. Anyone with root on the VPS sees them — acceptable since they already control the WG hub, but worth knowing.vps/02-harden.shhandles the host-level basics: key-only SSH, NOPASSWD sudo removal, ufw default-deny inbound, fail2ban on the sshd jail. Run it after01.- The VPS↔Pi tunnel uses a PresharedKey on top of Noise IK — a hedge against future cryptanalysis of Curve25519 ("harvest now, decrypt later").
- The Pi rejects forwarded traffic to RFC1918 destinations (
10/8,172.16/12,192.168/16) so a VPN client can't pivot to your home LAN through the tunnel. - Ubuntu cloud images enable
unattended-upgradesby default. Verify withsystemctl is-active unattended-upgrades.