Site-to-Site VPN with WireGuard Between Home Labs
If you have homelab gear in two locations — your house and a friend's, a main lab and a colocation, or a home network and a cloud VPC — you probably want those networks to talk to each other as if they were one. A site-to-site VPN does exactly that: it connects two entire subnets through an encrypted tunnel, so devices on one side can reach devices on the other side transparently.
WireGuard is the best tool for this. It's fast, simple, built into the Linux kernel, and the configuration is measured in lines, not pages. Unlike OpenVPN or IPsec, there's no certificate authority to manage, no complex phase negotiations, and the performance overhead is minimal — WireGuard typically saturates your internet uplink before it becomes a bottleneck itself.

Network Layout
For this guide, we'll connect two sites:
- Site A — Home network:
192.168.1.0/24, WireGuard gateway at192.168.1.1 - Site B — Remote network:
192.168.2.0/24, WireGuard gateway at192.168.2.1 - Tunnel network:
10.0.0.0/30(just two IPs for the tunnel endpoints) - Site A public IP:
203.0.113.10(or dynamic DNS hostname) - Site B public IP:
198.51.100.20(or dynamic DNS hostname)
After setup, a device at 192.168.1.50 (Site A) will be able to reach 192.168.2.100 (Site B) directly, and vice versa.
Installing WireGuard
On Debian/Ubuntu:
sudo apt update
sudo apt install -y wireguard
On Fedora:
sudo dnf install -y wireguard-tools
WireGuard is in the kernel since Linux 5.6. If you're on an older kernel, the wireguard-dkms package provides the module. Modern distros should have it built in.
Generating Keys
On each site's gateway, generate a key pair:
wg genkey | tee privatekey | wg pubkey > publickey
This creates two files: privatekey and publickey. You'll need Site A's public key on Site B's config, and vice versa. Keep the private keys secure.
# Read the keys
cat privatekey
cat publickey
Do this on both Site A and Site B gateways.
Configuring Site A
Create /etc/wireguard/wg0.conf on the Site A gateway:
[Interface]
Address = 10.0.0.1/30
PrivateKey = <Site A private key>
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey = <Site B public key>
Endpoint = 198.51.100.20:51820
AllowedIPs = 10.0.0.2/32, 192.168.2.0/24
PersistentKeepalive = 25
Key details:
- Address — The tunnel IP for this side. Using a /30 gives us exactly two usable IPs.
- ListenPort — The UDP port WireGuard listens on. You'll need to forward this on your router.
- PostUp/PostDown — Firewall rules that enable forwarding and NAT when the tunnel comes up. Replace
eth0with your actual WAN interface name. - AllowedIPs — This is both an ACL and a routing table. It tells WireGuard which destination IPs should go through the tunnel. Here, we route the tunnel subnet and the entire remote LAN.
- PersistentKeepalive — Sends a keepalive packet every 25 seconds. Essential when one or both sides are behind NAT.
Configuring Site B
Create /etc/wireguard/wg0.conf on the Site B gateway:
[Interface]
Address = 10.0.0.2/30
PrivateKey = <Site B private key>
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
[Peer]
PublicKey = <Site A public key>
Endpoint = 203.0.113.10:51820
AllowedIPs = 10.0.0.1/32, 192.168.1.0/24
PersistentKeepalive = 25
It's a mirror of Site A's config with the IPs and keys swapped.
Enabling IP Forwarding
Both gateways need to forward packets between interfaces. Enable it:
# Enable immediately
sudo sysctl -w net.ipv4.ip_forward=1
# Make it persistent
echo "net.ipv4.ip_forward = 1" | sudo tee /etc/sysctl.d/99-wireguard.conf
sudo sysctl --system
Starting the Tunnel
On both sites:
sudo wg-quick up wg0
Enable it at boot:
sudo systemctl enable wg-quick@wg0
Verify the tunnel is up:
sudo wg show
You should see the peer listed with a recent handshake timestamp:
interface: wg0
public key: <your public key>
private key: (hidden)
listening port: 51820
peer: <remote public key>
endpoint: 198.51.100.20:51820
allowed ips: 10.0.0.2/32, 192.168.2.0/24
latest handshake: 12 seconds ago
transfer: 1.24 KiB received, 1.08 KiB sent
Test connectivity:
# From Site A, ping Site B's tunnel IP
ping 10.0.0.2
# From Site A, ping a device on Site B's LAN
ping 192.168.2.100
Routing for Other Devices
The tunnel is up between the two gateways, but other devices on each LAN don't know about the remote subnet yet. There are three approaches:
Option 1: Static Routes on Your Router (Recommended)
On your Site A router, add a static route:
Destination: 192.168.2.0/24
Gateway: 192.168.1.1 (your WireGuard gateway's LAN IP)
Do the same on Site B's router for 192.168.1.0/24 via 192.168.2.1. Now every device on both LANs can reach the other side without any per-device configuration.
Option 2: Static Routes on Individual Devices
If you can't modify your router:
# On a Linux device at Site A
sudo ip route add 192.168.2.0/24 via 192.168.1.1
# On Windows at Site A
route add 192.168.2.0 mask 255.255.255.0 192.168.1.1
Option 3: NAT/Masquerade (Already Configured)
The MASQUERADE rule in our PostUp commands means traffic from remote devices appears to come from the gateway's LAN IP. This works without any routing changes on other devices, but remote devices can't initiate connections back — they only see the gateway's IP, not individual remote devices. This is fine for accessing services but breaks things that need true bidirectional connectivity.
For a proper site-to-site setup, Option 1 (router static routes) is the way to go.
Port Forwarding
Both sites need UDP port 51820 forwarded from the public IP to the WireGuard gateway. If your gateway IS the router (running pfSense, OPNsense, etc.), no port forward is needed — just open the port in the firewall.
If one side has no static IP, use dynamic DNS. WireGuard will resolve the Endpoint hostname periodically:
[Peer]
Endpoint = mylab.duckdns.org:51820
If both sides are behind CGNAT (no public IP at all), you'll need a relay. A cheap VPS in the middle can act as a WireGuard hub that both sites connect to. Each site configures the VPS as its peer, and the VPS routes between them.
Dynamic DNS and Endpoint Refresh
WireGuard resolves the Endpoint hostname only at startup. If a dynamic IP changes, the tunnel breaks until you restart. Fix this with a cron job:
# /etc/cron.d/wireguard-reresolve
*/5 * * * * root /usr/bin/wg set wg0 peer <PEER_PUBLIC_KEY> endpoint $(dig +short mylab.duckdns.org):51820
Or use the reresolve-dns.sh script that ships with wireguard-tools:
*/5 * * * * root /usr/share/wireguard-tools/examples/reresolve-dns/reresolve-dns.sh wg0
Firewall Considerations
If you're running a firewall on the gateways (you should be), make sure these rules are in place:
# Allow WireGuard UDP port
sudo ufw allow 51820/udp
# Allow forwarding between wg0 and your LAN interface
sudo ufw route allow in on wg0 out on eth0
sudo ufw route allow in on eth0 out on wg0
If using firewalld:
sudo firewall-cmd --permanent --add-port=51820/udp
sudo firewall-cmd --permanent --add-masquerade
sudo firewall-cmd --reload
Monitoring the Tunnel
WireGuard doesn't have built-in monitoring, but you can script it:
#!/bin/bash
# Check if the latest handshake was within the last 3 minutes
LAST_HANDSHAKE=$(sudo wg show wg0 latest-handshakes | awk '{print $2}')
NOW=$(date +%s)
DIFF=$((NOW - LAST_HANDSHAKE))
if [ $DIFF -gt 180 ]; then
echo "WireGuard tunnel stale - last handshake ${DIFF}s ago"
# Optionally restart: systemctl restart wg-quick@wg0
fi
Run this from cron or a monitoring system. You can also export WireGuard metrics to Prometheus with wireguard_exporter.
Performance
WireGuard's overhead is minimal. On modern hardware, expect:
- Throughput: Limited by your internet connection, not WireGuard. On a LAN link, WireGuard can push multiple gigabits per second.
- Latency: Adds 1-2 ms on a LAN, plus whatever your internet latency is between sites.
- CPU: Negligible. WireGuard uses ChaCha20 encryption, which is extremely fast on CPUs without AES-NI, and fast everywhere else.
A Raspberry Pi can handle a WireGuard site-to-site tunnel at 300+ Mbps. A modest x86 box won't even notice it's there.
WireGuard site-to-site is one of those setups where the configuration time is measured in minutes but the utility lasts years. Once it's up, your two networks just work as one, and you stop thinking about which site a service is on.