← Posts

Point-to-Point WireGuard VPN on Raspberry Pi

September 1, 2024 · 7 min read

Why I Needed This

I had a few services running on my Raspberry Pi at home - a Samba share, some Docker containers, a couple of side projects. They worked great on the local network, but I had no way to reach them when I was out. Port forwarding felt like poking holes in my firewall for each service individually. What I wanted was a single encrypted tunnel back into my home network.

WireGuard kept coming up in everything I read. It’s a modern VPN protocol - small codebase (~4,000 lines of kernel code), fast cryptographic handshake, and dead simple configuration compared to OpenVPN or IPSec. The whole thing runs as a kernel module, so there’s almost no overhead.

What I Used

  • Raspberry Pi 5 (any Pi 4+ works)
  • Ethernet connection (Wi-Fi works but I wanted stability for a VPN endpoint)
  • Client device - my phone and MacBook
  • WireGuard app on the client side

How WireGuard Works

WireGuard is peer-to-peer - there’s technically no “server” and “client”, just peers that exchange public keys. But for this setup, the Pi acts as the always-on endpoint (I’ll call it the server) and my phone/laptop connects to it (the client).

sequenceDiagram
  autonumber

  participant Client as Client Device
  participant Router as Home Router
  participant Pi as Raspberry Pi

  Note over Client, Pi: One-time setup (manual key exchange)
  Client -->> Pi: Share client public key
  Pi -->> Client: Share server public key

  Note over Client, Pi: Connection (automatic)
  Client ->> Router: UDP 51820 (encrypted)
  Router ->> Pi: Port forward
  Pi -->> Router: Encrypted response
  Router -->> Client: Encrypted response

  Note over Client, Pi: Ongoing communication
  Client ->> Router: Encrypted traffic
  Router ->> Pi: Forward to 10.0.0.1
  Pi -->> Client: Encrypted traffic

The key exchange happens once during setup. After that, WireGuard handles handshakes automatically - it re-negotiates every few minutes for forward secrecy.

Setting Up the Server (Raspberry Pi)

Install WireGuard

sudo apt update && sudo apt upgrade -y
sudo apt install -y wireguard

WireGuard is in the official repos for Raspberry Pi OS, so this just works.

Generate Keys

WireGuard uses Curve25519 key pairs. The umask 077 ensures the private key file is only readable by root.

umask 077
wg genkey | sudo tee /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key

Configure the Interface

I created the WireGuard config file:

sudo vim /etc/wireguard/wg0.conf
[Interface]
Address = 10.0.0.1/24
ListenPort = 51820
PrivateKey = $(cat /etc/wireguard/private.key)

# Client configurations will be added here later

A quick breakdown of what these do:

FieldValuePurpose
Address10.0.0.1/24Pi’s IP on the VPN subnet (range: 10.0.0.1 - 10.0.0.254)
ListenPort51820UDP port WireGuard listens on
PrivateKeyfrom fileEncrypts all traffic from this peer

Start the Service

sudo systemctl enable --now wg-quick@wg0

Quick check to see if it’s running:

sudo wg show
interface: wg0
  public key: <SERVER_PUBLIC_KEY>
  private key: (hidden)
  listening port: 51820

I also verified the interface showed up in NetworkManager:

nmcli d
DEVICE         TYPE       STATE                   CONNECTION
wlan1          wifi       connected               Wi-Fi connection 1
lo             loopback   connected (externally)  lo
docker0        bridge     connected (externally)  docker0
wg0            wireguard  connected (externally)  wg0
eth0           ethernet   disconnected            --
wlan0          wifi       disconnected            --
p2p-dev-wlan0  wifi-p2p   disconnected            --

There’s wg0 - the VPN interface is live.

Setting Up the Client (Phone / Laptop)

I installed the WireGuard app on my phone from the App Store and on my MacBook from the official site.

The app generates a key pair automatically when you create a new tunnel. I filled in the config:

[Interface]
PrivateKey = <CLIENT_PRIVATE_KEY, AUTO_GENERATED>
Address = 10.0.0.2/24
DNS = 1.1.1.1, 8.8.8.8

[Peer]
PublicKey = <SERVER_PUBLIC_KEY>
Endpoint = <SERVER_PUBLIC_IP>:51820
AllowedIPs = 10.0.0.0/24

The important fields:

FieldWhat It Does
AddressClient’s IP on the VPN subnet (10.0.0.2)
DNSDNS servers to use while connected
PublicKeyThe Pi’s public key (from /etc/wireguard/public.key)
EndpointThe Pi’s public IP (or local IP if on the same network)
AllowedIPsWhich traffic goes through the tunnel

Setting AllowedIPs = 10.0.0.0/24 means only VPN subnet traffic gets tunneled. If you want all internet traffic routed through the Pi (useful on public Wi-Fi), change it to 0.0.0.0/0.

Connecting the Pieces

Add the Client to the Server

Now that the client has a public key, I added it to the server’s config:

[Peer]
PublicKey = <CLIENT_PUBLIC_KEY>
AllowedIPs = 10.0.0.2/32

The /32 means this peer can only use the single IP 10.0.0.2 - not a range. This is important for security.

Then restart the service:

sudo systemctl restart wg-quick@wg0

Making It Reachable from Outside

Everything so far only works on the local network. To connect from outside (coffee shop, office, mobile data), two things need to happen.

1. Port forwarding on the router

Log into your router’s admin panel and forward UDP port 51820 to the Pi’s local IP address. For my setup:

SettingValue
ProtocolUDP
External port51820
Internal IP192.168.1.x (Pi’s local IP)
Internal port51820

This is the only port you need to open - WireGuard handles everything over a single UDP port, which is one of the things I like about it compared to OpenVPN’s multi-port setups.

2. Finding your public IP

Your client needs to know your home’s public IP for the Endpoint field. You can find it by running this on the Pi:

curl -s ifconfig.me

The problem is most ISPs assign dynamic IPs that change periodically. I dealt with this by setting up a free DDNS service - it gives you a hostname like myhome.ddns.net that always points to your current public IP. You can use that as the endpoint instead:

Endpoint = myhome.ddns.net:51820

Most routers have built-in DDNS support (No-IP, DuckDNS, etc.), so the IP updates automatically whenever it changes.

graph LR
    Client[Client Device] -->|Internet| DDNS[DDNS / Public IP]
    DDNS -->|UDP 51820| Router[Home Router]
    Router -->|Port Forward| Pi[Raspberry Pi<br/>192.168.1.x]

Testing It

Activated the tunnel on my phone and pinged the Pi:

ping -c 3 10.0.0.1
PING 10.0.0.1 (10.0.0.1): 56 data bytes
64 bytes from 10.0.0.1: icmp_seq=0 ttl=64 time=8.444 ms
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=11.057 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=30.645 ms

--- 10.0.0.1 ping statistics ---
3 packets transmitted, 3 packets received, 0.0% packet loss
round-trip min/avg/max/stddev = 8.444/16.715/30.645/9.907 ms

Three packets, zero loss. To make sure it wasn’t just ICMP working, I started a quick HTTP server on the Pi:

python3 -m http.server 8000

Opened 10.0.0.1:8000 in my phone’s browser - the directory listing loaded. The tunnel was working end to end.

The Full Picture

Here’s how everything fits together:

graph TD
    Phone[Phone<br/>10.0.0.2] -->|WireGuard tunnel| Internet[Internet]
    Laptop[MacBook<br/>10.0.0.3] -->|WireGuard tunnel| Internet
    Internet -->|UDP 51820| Router[Home Router]
    Router -->|Port Forward| Pi[Raspberry Pi<br/>10.0.0.1]
    Pi --> Samba[Samba Share]
    Pi --> Docker[Docker Services]
    Pi --> Web[Web Apps]

One tunnel in, access to everything on the Pi. No per-service port forwarding needed.

What I’d Do Differently

  • More peers: Adding a second client (like my work laptop) is just another [Peer] block in the server config with a unique IP.
  • Firewall rules: Right now the VPN subnet has full access to the Pi. For a multi-user setup, I’d want iptables rules to restrict what each peer can reach.
  • Split tunneling: Right now I either tunnel VPN traffic only or everything. A more granular setup could route specific services through the tunnel while keeping the rest direct.

References