11 KiB
Raspberry Pi VPN (Networking)
In this lesson you'll build a personal VPN server on your Raspberry Pi using WireGuard — the modern standard for VPNs. It gives you a private road into your home network: when you're at school, at a friend's house, or on vacation, you can reach devices on your home Wi-Fi — the Pi, a NAS, a media server, a smart home dashboard — as if your phone were plugged into the house.
As a side effect, the road is encrypted, so nobody in between can read what you're accessing.
How it works
A quick networking refresher so the steps make sense:
- Devices on your home network get local IP addresses like
192.168.1.42. To the internet, your whole house appears as one public IP address (the router's). - By default the internet can only knock on the front door — the router. To reach devices inside the house, we set up one exception: the Pi runs a VPN server, and the router forwards that one port to it.
- A VPN creates an encrypted tunnel between your phone and the Pi. Inside the
tunnel we use our own private addresses: the Pi is
10.100.0.1, your phone is10.100.0.2. - When you connect, your phone has two identities at once: it's on your carrier's
network and on your home network. Anything in your home range
(
192.168.1.x) travels through the tunnel into the house.
phone --encrypted tunnel--> Pi --home network--> NAS, Pi, media server, ...
Check your own network first: find your Pi's local IP on the Pi with
hostname -I. This lesson assumes it looks like 192.168.1.42, so the home
range is 192.168.1.0/24. If your router uses a different range (some use
192.168.0.x or 10.0.0.x), substitute yours everywhere.
What you need
- A Raspberry Pi running Raspberry Pi OS, connected to your home network (wired ethernet is best and easiest)
- Your phone, with the official WireGuard app (iOS or Android)
- Access to your router's admin page (usually
192.168.1.1in a browser)
Step 1 — Install WireGuard on the Pi
sudo apt update
sudo apt install -y wireguard wireguard-tools nftables qrencode
(qrencode prints a scannable QR code; nftables handles the forwarding rules
later.)
Step 2 — Create the server's keys
Every WireGuard device has a private key (a secret) and a public key (safe to share). Think of them like a password and a public address. The Pi gets its pair now:
sudo -i
cd /etc/wireguard
umask 077
wg genkey | tee server.key | wg pubkey > server.pub
cat server.key
sudo -ilogs you in as the administrator account (root). Typeexitat the end of each root session to go back to your normal user.umask 077makes sure new files are only readable by you.- Copy the long string from
cat server.key— you'll paste it into the config in the next step. Never share your private key. The public key (server.pub) is fine to share.
Step 3 — Write the server config
Still as root, open the config file:
nano /etc/wireguard/wg0.conf
Type this, replacing the last line with your server's private key:
[Interface]
Address = 10.100.0.1/24, fd08:4711::1/64
ListenPort = 47111
PrivateKey = PASTE_YOUR_SERVER_PRIVATE_KEY_HERE
Save (Ctrl+O, Enter) and quit (Ctrl+X). What the lines mean:
Address— the Pi's address inside the tunnel (10.100.0.1).ListenPort— the port clients connect to.47111, UDP.
Step 4 — Let the Pi forward traffic
To send your phone's traffic on to other devices in the house, the Pi has to be willing to pass packets through. Two things enable that: IP forwarding (allow packets through) and NAT (let tunnel devices share the Pi's internet connection — you'll want it if you ever tunnel more than just home access).
First, edit /etc/sysctl.d/99-sysctl.conf (nano /etc/sysctl.d/99-sysctl.conf).
Make sure these two lines exist without a # in front of them (remove the #
if there is one):
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
Save and quit, then apply:
sysctl --system
Second, open the server config again (nano /etc/wireguard/wg0.conf) and add
these two lines inside the [Interface] section (paste them exactly as one line
each):
PostUp = nft add table ip wireguard; nft add chain ip wireguard wireguard_chain {type nat hook postrouting priority srcnat\; policy accept\;}; nft add rule ip wireguard wireguard_chain counter packets 0 bytes 0 masquerade; nft add table ip6 wireguard; nft add chain ip6 wireguard wireguard_chain {type nat hook postrouting priority srcnat\; policy accept\;}; nft add rule ip6 wireguard wireguard_chain counter packets 0 bytes 0 masquerade
PostDown = nft delete table ip wireguard; nft delete table ip6 wireguard
PostUp/PostDown are commands that run automatically when the tunnel starts and
stops — here they add and remove the NAT rule. Ugly-looking, but it's copy-paste
and it's what the official docs recommend.
When you're done, type exit to leave root.
Step 5 — Start the VPN
sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0
sudo wg
If sudo wg prints an interface called wg0 with a public key and a listening
port, the server is up. enable makes it start automatically after every reboot,
so you only ever run the start command after changing the config.
Step 6 — Open a door in the router
Your phone needs to reach the Pi through the internet. Two things in your router's admin page:
- Give the Pi a fixed local IP. Find the current one on the Pi with
hostname -I, then add a DHCP reservation in the router (binds that IP to the Pi's MAC address). If the Pi's IP ever changes, the port forward breaks. - Add a port forward: UDP port
47111→ the Pi's local IP.
Also find your home's public IP — it's often shown on the router's front page
("WAN IP"), or open https://ifconfig.co on any device at home. You'll paste it
into the client config next.
Step 7 — Add your phone as a client
WireGuard calls every connected device a peer. Back on the Pi:
sudo -i
cd /etc/wireguard
umask 077
name=phone
wg genkey | tee ${name}.key | wg pubkey > ${name}.pub
wg genpsk > ${name}.psk
That created the phone's key pair plus a preshared key (an extra per-device layer of encryption).
Register the phone in the server config — this appends a [Peer] section to
wg0.conf:
echo "[Peer]" >> wg0.conf
echo "PublicKey = $(cat ${name}.pub)" >> wg0.conf
echo "PresharedKey = $(cat ${name}.psk)" >> wg0.conf
echo "AllowedIPs = 10.100.0.2/32, fd08:4711::2/128" >> wg0.conf
Then tell the running server about the change (this reloads the config without kicking off connected clients):
wg syncconf wg0 <(wg-quick strip wg0)
Now build the config file the phone will use:
echo "[Interface]" > ${name}.conf
echo "Address = 10.100.0.2/32, fd08:4711::2/128" >> ${name}.conf
echo "PrivateKey = $(cat ${name}.key)" >> ${name}.conf
echo "" >> ${name}.conf
echo "[Peer]" >> ${name}.conf
echo "PublicKey = $(cat server.pub)" >> ${name}.conf
echo "PresharedKey = $(cat ${name}.psk)" >> ${name}.conf
echo "Endpoint = 203.0.113.10:47111" >> ${name}.conf
echo "AllowedIPs = 10.100.0.0/24, fd08:4711::/64, 192.168.1.0/24" >> ${name}.conf
echo "PersistentKeepalive = 25" >> ${name}.conf
Replace 203.0.113.10 with your home's public IP, and 192.168.1.0/24 with
your home range if it's different. What the important lines do:
AllowedIPsdecides what travels through the tunnel. Here it's the tunnel itself plus your whole home range — so home devices are reachable. Everything else (normal web browsing, apps) goes over your carrier's network directly, exactly as usual. (If you ever want everything tunneled — e.g. to route it through a future Pi-hole — you'd replace that line withAllowedIPs = 0.0.0.0/0, ::/0.)PersistentKeepalive = 25— phones sit behind carriers' NAT, so this sends a small packet every 25 seconds to keep the tunnel's door open.
Step 8 — Connect your phone
Still as root, print the config as a QR code:
qrencode -t ansiutf8 < ${name}.conf
On your phone: open the WireGuard app → tap the blue + → scan the QR code →
toggle the tunnel on. (If you're using the Pi over SSH from a laptop, the QR
code appears in your terminal window — just scan it with the phone.) Then exit
root.
Step 9 — Check it works
The interesting test is from outside the house: turn your phone's Wi-Fi off and use cellular data.
- On the Pi, run
sudo wg. Your phone should appear withlatest handshake: X seconds agoand a transfer count. - Open your Pi's local address in your phone's browser —
http://192.168.1.42orhttp://192.168.1.42/admin(use your Pi's own local IP). If the page loads from a school Wi-Fi network, your phone is effectively sitting in your house. - Try SSH:
ssh YOUR_USER@192.168.1.42from Termux (Android) or any SSH client on iPhone. If you get a shell, remote access is fully working.
What you can do now
Rule of thumb: anywhere you would have typed a 192.168.1.x address while at
home, that same address now works from anywhere in the world.
- Web interfaces: Pi-hole admin, your NAS, Home Assistant — open them in the browser.
- SSH into any device in the house.
- Stream from a media server or download files from a NAS.
One caveat: device names like raspberrypi.local only resolve inside the
house, so over the tunnel, use IP addresses.
Troubleshooting
- Phone never shows a handshake: check the port forward is UDP (not TCP) on
port
47111pointing at the Pi's current local IP; check theEndpointline has your current public IP (it can change — call your ISP for a static one if it's changing often); check keys were copied without extra spaces. - Can't connect from home Wi-Fi, but cellular works: common and often unfixable — many routers can't loop a forwarded port back to devices already inside the network (no "NAT loopback"). Test with cellular data.
- Connected, but home devices are unreachable: the
AllowedIPsline in the client config has the wrong home range — it must match your router's subnet, the same range your Pi's IP lives in. Also make sure Step 4 (IP forwarding) is done. - A device works at home but not over the tunnel: use its IP address instead of its name (see the caveat above), and double-check the device is actually on your home network.
Security notes
- Anyone with your phone's config file can walk into your home network. Treat it like a password — don't screenshot it into a shared chat.
- Your Pi is now reachable from the internet. Keeping a firewall enabled (for
example with
ufw) is a good next step. - Lost a device? Delete its
[Peer]section fromwg0.confand runwg syncconf wg0 <(wg-quick strip wg0)again — its keys stop working.