WireGuard Raspberry Pi Site-to-Site VPN: Complete Setup Guide

WireGuard Raspberry Pi Site‑to‑Site VPN Setup

WireGuard Raspberry Pi site-to-site VPN bridges two private networks across the internet using a Pi 4 as a dedicated VPN endpoint. WireGuard uses Curve25519 for key exchange and ChaCha20-Poly1305 for encryption, runs as a kernel module on Raspberry Pi OS Bookworm, and handles routing and NAT through standard Linux tools. This guide covers key generation, server and client configuration, firewall rules using PostUp hooks, persistence via systemd, and a watchdog timer for automatic reconnection.

Last tested: Raspberry Pi OS Bookworm Lite 64-bit | March 15, 2026 | Raspberry Pi 4 Model B (4GB) server + Raspberry Pi 4 Model B (2GB) travel client | WireGuard 1.0.20210914

Key Takeaways

  • WireGuard requires one side to have a reachable public IP or a Dynamic DNS hostname with port 51820/UDP forwarded. The travel client can be behind NAT with no public IP. Only the server needs to be reachable.
  • Do not use SaveConfig = true in production configs. It overwrites the config file with runtime state when wg-quick down is run, silently discarding comments and any settings not reflected in the current interface state.
  • Use PostUp and PostDown hooks in wg0.conf for NAT rules rather than running iptables commands separately. This keeps all routing logic in one file and ensures rules are removed cleanly when the interface goes down.
  • Choose a VPN subnet that does not overlap with either site’s LAN. Using 10.13.13.0/24 for the tunnel avoids conflicts with the common 192.168.1.x and 10.0.0.x ranges used by home routers.
  • PersistentKeepalive = 25 is required on the client side when it is behind NAT. Without it, the NAT mapping expires and the tunnel goes silent after a period of inactivity.
  • Test the tunnel manually with wg-quick up wg0 before enabling the systemd service. A broken config that auto-starts drops SSH access on reboot.
WireGuard Raspberry Pi site-to-site VPN diagram showing home Pi server and travel Pi client connected through encrypted WireGuard tunnel with LAN routing

How WireGuard Raspberry Pi Site-to-Site VPN Works

WireGuard creates a virtual network interface (wg0) on each Pi. When a packet is destined for an IP in the AllowedIPs list of a peer, WireGuard encrypts it and sends it as a UDP datagram to that peer’s endpoint. The receiving Pi decrypts it and forwards it to the local network. The result is that devices on both LANs can reach each other as if they were on the same network.

In a site-to-site setup, the home Pi is the server: it has a fixed public IP (or Dynamic DNS hostname) and listens on UDP 51820. The travel Pi is the client: it initiates the connection from wherever it happens to be, even behind NAT. The PersistentKeepalive setting on the client sends a small packet every 25 seconds to maintain the NAT mapping.

ComponentHome Pi (server)Travel Pi (client)
WireGuard tunnel IP10.13.13.1/2410.13.13.2/24
LAN subnet192.168.1.0/24192.168.2.0/24
ListenPort51820Not required
Endpoint in peer configNot requiredhome.example.com:51820
PersistentKeepaliveNot required25
Public IP requiredYes (or DDNS)No
Wireguard diagram flow

Hardware and OS Requirements

Pi 4 with 2GB RAM handles WireGuard comfortably alongside routing and dnsmasq. Pi Zero 2 W works for low-traffic links but its single-core CPU and Wi-Fi-only connectivity limit practical use. For the travel client, Pi 4 with a USB Ethernet adapter (see USB NIC setup) gives the most reliable setup: Wi-Fi for WAN (hotel/café) and Ethernet for LAN (connected devices). For the home server, Pi 4 on wired Ethernet is strongly preferred over Wi-Fi for stability.

Flash Raspberry Pi OS Bookworm Lite 64-bit using Raspberry Pi Imager. In Imager’s advanced settings, set a hostname, enable SSH, and configure credentials. There is no need to edit boot partition files manually. After first boot on both Pis:

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

Expected result: wg --version returns without error on both Pis.

Step 1: Generate Keys on Both Pis

Run the following on each Pi independently. Never copy a private key between machines.

# Generate private key and derive public key
wg genkey | sudo tee /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key

# Restrict permissions -- private key must not be world-readable
sudo chmod 600 /etc/wireguard/private.key

# View the keys (you will need both values for the config files)
sudo cat /etc/wireguard/private.key
sudo cat /etc/wireguard/public.key

Optionally generate a pre-shared key for an additional symmetric encryption layer. Run this once and use the output in both peer configs:

wg genpsk | sudo tee /etc/wireguard/preshared.key
sudo chmod 600 /etc/wireguard/preshared.key

Expected result: Four files exist: two key pairs, one on each Pi. Each private key is 44 characters of base64. Each public key is also 44 characters but different from the private key. The preshared key file (if used) contains a single 44-character base64 string.

Step 2: Configure the Home Pi (Server)

Create /etc/wireguard/wg0.conf on the home Pi. Replace the placeholder values with the actual keys generated in Step 1:

sudo nano /etc/wireguard/wg0.conf
[Interface]
# Home Pi tunnel IP
Address = 10.13.13.1/24

# Home Pi private key
PrivateKey = <HOME_PI_PRIVATE_KEY>

# WireGuard listens on this UDP port
ListenPort = 51820

# Enable IP forwarding and NAT when the interface comes up
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -A FORWARD -o wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -o wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE

[Peer]
# Travel Pi public key
PublicKey = <TRAVEL_PI_PUBLIC_KEY>

# Optional: pre-shared key for extra encryption layer
# PresharedKey = <PRESHARED_KEY>

# Travel Pi tunnel IP + travel site LAN
AllowedIPs = 10.13.13.2/32, 192.168.2.0/24

Restrict the config file permissions:

sudo chmod 600 /etc/wireguard/wg0.conf

On the home network router, forward UDP port 51820 to the home Pi’s LAN IP. If your ISP does not provide a static public IP, set up a Dynamic DNS hostname (e.g. via Duck DNS or Cloudflare) pointing to your home IP. The travel Pi uses this hostname as its endpoint.

Step 3: Configure the Travel Pi (Client)

sudo nano /etc/wireguard/wg0.conf
[Interface]
# Travel Pi tunnel IP
Address = 10.13.13.2/24

# Travel Pi private key
PrivateKey = <TRAVEL_PI_PRIVATE_KEY>

# Enable forwarding and NAT for connected LAN clients
PostUp = sysctl -w net.ipv4.ip_forward=1
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -A FORWARD -o wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -D FORWARD -o wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE

[Peer]
# Home Pi public key
PublicKey = <HOME_PI_PUBLIC_KEY>

# Optional pre-shared key (must match server)
# PresharedKey = <PRESHARED_KEY>

# Home Pi public address and WireGuard port
Endpoint = home.example.com:51820

# Route home tunnel IP + home LAN through the tunnel
# Use 0.0.0.0/0 instead to route ALL traffic through the tunnel
AllowedIPs = 10.13.13.1/32, 192.168.1.0/24

# Required when client is behind NAT
PersistentKeepalive = 25
sudo chmod 600 /etc/wireguard/wg0.conf

Split tunnel vs full tunnel

ModeAllowedIPs on clientEffect
Split tunnel10.13.13.1/32, 192.168.1.0/24Only traffic to home network goes through VPN. Internet traffic uses local connection.
Full tunnel0.0.0.0/0All traffic routes through home Pi. Internet traffic exits from home IP.

Step 4: Enable IP Forwarding Persistently

The PostUp hooks set IP forwarding at interface startup, but this resets on reboot if not made permanent. Set it persistently so it survives reboots regardless of whether the WireGuard interface is up:

echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf

Step 5: Bring Up the Tunnel and Test

Start the server interface first, then the client:

# On the home Pi (server)
sudo wg-quick up wg0

# On the travel Pi (client)
sudo wg-quick up wg0

Verify the handshake

# On either Pi -- check tunnel status
sudo wg show

# Healthy output includes:
# latest handshake: X seconds ago
# transfer: X MiB received, X MiB sent

If latest handshake shows “never”, the peers have not connected. See the troubleshooting section.

Ping tests

# From the travel Pi -- ping the home Pi tunnel IP
ping 10.13.13.1

# From the travel Pi -- ping a device on the home LAN
ping 192.168.1.x

# From the home Pi -- ping the travel Pi tunnel IP
ping 10.13.13.2

# Verify which public IP your traffic exits from (full tunnel only)
curl -s ifconfig.me

Expected result: sudo wg show shows a handshake timestamp within the last 30 seconds. Pings to both tunnel IPs succeed. If using a full tunnel, curl ifconfig.me returns the home network’s public IP.

Step 6: Make the Tunnel Persistent

Enable the wg-quick systemd service to start the tunnel at boot. Do this only after confirming the tunnel works correctly. An auto-starting broken config drops SSH access on reboot:

sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0
sudo systemctl status wg-quick@wg0

Watchdog timer for automatic reconnection

Create a watchdog script that restarts the tunnel if the peer becomes unreachable. Save it as /usr/local/bin/wg-watchdog.sh:

#!/bin/bash
# Ping the peer tunnel IP -- restart wg0 if unreachable
PEER_IP="10.13.13.1"  # Change to 10.13.13.2 on the server

if ! ping -c 2 -W 3 "$PEER_IP" >/dev/null 2>&1; then
  echo "$(date): WireGuard peer unreachable. Restarting wg0." >> /var/log/wg-watchdog.log
  systemctl restart wg-quick@wg0
fi
sudo chmod 750 /usr/local/bin/wg-watchdog.sh

Create a systemd service and timer pair. Service at /etc/systemd/system/wg-watchdog.service:

[Unit]
Description=WireGuard watchdog
After=wg-quick@wg0.service

[Service]
Type=oneshot
ExecStart=/usr/local/bin/wg-watchdog.sh

Timer at /etc/systemd/system/wg-watchdog.timer:

[Unit]
Description=Run WireGuard watchdog every 5 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min

[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now wg-watchdog.timer

Expected result: systemctl list-timers wg-watchdog.timer shows the timer active with a next trigger time. After an intentional wg-quick down wg0, the watchdog restarts the tunnel within 5 minutes.

Travel Router Setup

The travel Pi connects to a hotel or café network on its WAN interface (typically wlan0 or a USB Ethernet adapter) and shares the VPN-tunnelled connection to devices plugged into its LAN port or Wi-Fi hotspot. Install dnsmasq to serve DHCP to connected devices (see Raspberry Pi Router guide for full dnsmasq setup):

sudo apt install dnsmasq -y

Add a DHCP pool for the travel LAN interface in /etc/dnsmasq.conf:

# Listen on LAN interface
interface=eth1
bind-interfaces
dhcp-range=192.168.2.100,192.168.2.200,12h
dhcp-option=3,192.168.2.1
dhcp-option=6,192.168.2.1
server=1.1.1.1

Assign a static IP to the LAN interface on Bookworm with nmcli:

sudo nmcli connection modify "Wired connection 2" \
  ipv4.method manual \
  ipv4.addresses 192.168.2.1/24

sudo nmcli connection up "Wired connection 2"
sudo systemctl enable --now dnsmasq

Devices connected to the travel Pi’s LAN port receive a 192.168.2.x address, route through the WireGuard tunnel, and emerge on the home network. For captive portal scenarios (hotel login pages), temporarily bring the tunnel down with sudo wg-quick down wg0, authenticate through the captive portal from the travel Pi’s browser or a directly connected device, then bring the tunnel back up.

Monitoring

# Live tunnel status (handshake time, bytes transferred, endpoint)
sudo wg show

# Follow systemd service logs
sudo journalctl -u wg-quick@wg0 -f

# Check watchdog log
tail -f /var/log/wg-watchdog.log

# Bandwidth monitoring per interface
sudo apt install vnstat -y
vnstat -i wg0

A healthy tunnel shows a latest handshake timestamp within the last 25-130 seconds (depending on PersistentKeepalive interval and traffic). A handshake older than 3 minutes with no traffic indicates the tunnel has dropped.

Security Hardening

Firewall on the home server

sudo apt install ufw -y
sudo ufw default deny incoming
sudo ufw allow OpenSSH
sudo ufw allow 51820/udp
sudo ufw enable

SSH key authentication

# Copy your public key to both Pis
ssh-copy-id pi@homepi.local
ssh-copy-id pi@travelpi.local

# Disable password login
sudo nano /etc/ssh/sshd_config
# Set: PasswordAuthentication no
sudo systemctl restart sshd

Key rotation

Generate new keypairs and update both configs to rotate credentials. Bring the interface down, replace the keys, update the peer’s PublicKey with the new value, and bring the interface back up. The tunnel re-establishes using the new keys. Old public keys have no value once removed from the peer config.

Troubleshooting

Handshake never completes

# Confirm the server is reachable from the client
nc -vzu home.example.com 51820

# Check the server is actually listening
sudo ss -ulnp | grep 51820

# Confirm port forward on home router is pointing to Pi's LAN IP
# Confirm firewall allows 51820/UDP inbound on server

The most common cause is that port 51820/UDP is not forwarded to the home Pi on the router, or the home Pi firewall is blocking it. Verify with nc -vzu from the travel Pi. If it returns “open” or times out with no “refused” message, the port is reachable.

Tunnel connects but LAN traffic does not pass

# Confirm IP forwarding is on
sysctl net.ipv4.ip_forward
# Must return: net.ipv4.ip_forward = 1

# Confirm iptables FORWARD rules are present
sudo iptables -L FORWARD -v

# Confirm NAT rule is present
sudo iptables -t nat -L POSTROUTING -v

If the PostUp hooks fired correctly, the FORWARD and MASQUERADE rules will be present. If they are missing, the PostUp commands may have failed silently. Run them manually and check for errors.

Subnet conflict with hotel network

If the hotel assigns addresses in 192.168.1.x and your home LAN is also 192.168.1.x, the Pi cannot distinguish local traffic from VPN-bound traffic. The fix is to use a non-standard VPN tunnel subnet (10.13.13.0/24 as in this guide) and ensure each site’s LAN uses a different range. If you are in the field and hit this problem, bring the tunnel down, connect directly to the hotel network, and plan a subnet change before the next trip.

Large packet drops or SSH lag

# Test MTU -- if ping fails at 1500 but works at 1400, MTU is the issue
ping -M do -s 1472 10.13.13.1

# Set a lower MTU in wg0.conf [Interface] section
# MTU = 1380

WireGuard adds 60 bytes of overhead to each packet (32 bytes header + 16 bytes authentication tag + tunnel encapsulation). The default MTU of 1420 handles most cases, but some networks with additional encapsulation (hotel VLANs, PPPoE links) require a lower value.

WireGuard vs Other VPN Options

FeatureWireGuardOpenVPNIPsec
Codebase size~4,000 lines~100,000+ linesVariable
ProtocolUDP onlyUDP or TCPUDP/ESP
Key exchangeCurve25519TLS/RSAIKEv2
EncryptionChaCha20-Poly1305AES (configurable)AES (configurable)
Pi CPU overheadLowMedium-HighMedium
Config complexityLowHighHigh
Dynamic IP supportNeeds DDNS on serverNeeds DDNS on serverNeeds DDNS on server

Tailscale and ZeroTier are worth considering when the configuration complexity of WireGuard is the primary concern. Both build on WireGuard but add a coordination layer that handles NAT traversal, key distribution, and device discovery automatically. The trade-off is reliance on third-party infrastructure for the control plane. For a self-hosted site-to-site setup where one side has a reachable public IP, raw WireGuard as configured in this guide has no such dependency.

FAQ

Does WireGuard reconnect automatically after losing connection?

The client re-establishes the tunnel as soon as it can reach the server endpoint. PersistentKeepalive = 25 on the client ensures the NAT mapping stays active and the tunnel recovers quickly from brief outages. For longer outages or cases where the keepalive is insufficient, the watchdog timer described in Step 6 restarts the interface if the peer becomes unreachable.

Do I need a static public IP on the home server?

No. A Dynamic DNS hostname updated by a DDNS client on the home router is sufficient. Services like Duck DNS, Cloudflare, or No-IP provide free hostnames. The travel Pi uses the hostname in its Endpoint setting. WireGuard resolves the hostname each time it initiates a handshake, so DDNS updates propagate within one keepalive cycle.

Can I use a Pi Zero 2 W as the travel client?

Yes for low-traffic use cases. The Pi Zero 2 W handles WireGuard encryption adequately at typical VPN throughput speeds. The limitation is connectivity: it has only Wi-Fi and no Ethernet port. You need a USB OTG Ethernet adapter for the LAN side, which requires a powered hub if you also need USB power. Pi 4 with a USB Ethernet adapter is more reliable for sustained use.

Can I add more peers to the server later?

Yes. Add a new [Peer] section to the server wg0.conf for each additional client. Each peer needs a unique tunnel IP within the 10.13.13.0/24 range. Apply the change with sudo wg addpeer (no restart needed) or by restarting the interface. Do not use SaveConfig = true. Add peers to the file manually so the config remains authoritative.

What if both sites are behind NAT with no public IP?

Neither side can be the WireGuard server in the traditional sense. Options: use a cheap VPS as a relay (the VPS has a public IP and both Pis connect to it), use Tailscale which handles double-NAT traversal automatically, or obtain a static IP from your ISP. A $5/month VPS running WireGuard as a relay is the most common solution for this scenario.

References


About the Author

Chuck Wilson has been programming and building with computers since the Tandy 1000 era. His professional background includes CAD drafting, manufacturing line programming, and custom computer design. He runs PidiyLab in retirement, documenting Raspberry Pi and homelab projects that he actually deploys and maintains on real hardware. Every article on this site reflects hands-on testing on specific hardware and OS versions, not theoretical walkthroughs.

Last tested hardware: Raspberry Pi 4 Model B (4GB) server, Raspberry Pi 4 Model B (2GB) travel client. Last tested OS: Raspberry Pi OS Bookworm Lite 64-bit. WireGuard 1.0.20210914.