Set up secure network-to-network VPN tunnels using WireGuard with advanced routing, firewall rules, and monitoring for connecting multiple office locations or data centers.
Prerequisites
- Two Linux servers with public IP addresses
- Root or sudo access on both servers
- Basic understanding of networking and IP routing
- Firewall configuration knowledge
What this solves
WireGuard site-to-site VPN connections enable secure communication between entire networks rather than individual clients. This setup allows offices, data centers, or cloud environments to connect their internal networks securely over the internet, creating a unified private network infrastructure.
WireGuard site-to-site architecture and planning
Site-to-site VPN requires careful network planning to avoid IP conflicts and ensure proper routing. Each site needs a unique subnet, and both sites must route traffic destined for the remote network through their respective WireGuard interfaces.
This tutorial demonstrates connecting two sites with full network access between their internal subnets. Each site will act as a gateway for its local network, routing traffic through the WireGuard tunnel.
Step-by-step configuration
Install WireGuard on both sites
Install WireGuard and related networking tools on the gateway servers at both locations.
sudo apt update
sudo apt install -y wireguard wireguard-tools iptables-persistent
Generate key pairs for both sites
Create public and private key pairs for each site. Run these commands on each gateway server to generate its own keys.
sudo mkdir -p /etc/wireguard
sudo wg genkey | sudo tee /etc/wireguard/private.key | wg pubkey | sudo tee /etc/wireguard/public.key
sudo chmod 600 /etc/wireguard/private.key
sudo chmod 644 /etc/wireguard/public.key
Display the keys for configuration exchange between sites:
echo "Private key:" && sudo cat /etc/wireguard/private.key
echo "Public key:" && cat /etc/wireguard/public.key
Configure primary site WireGuard server
Configure the primary site (Site A) with network 192.168.1.0/24. This server will listen for connections from the secondary site.
[Interface]
PrivateKey = SITE_A_PRIVATE_KEY_HERE
Address = 10.100.0.1/30
ListenPort = 51820
SaveConfig = false
Enable IP forwarding for this interface
PreUp = sysctl -w net.ipv4.ip_forward=1
Add route to Site B's network
PostUp = ip route add 192.168.2.0/24 dev wg0
Configure firewall rules for forwarding
PostUp = iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT
PostUp = iptables -A FORWARD -i eth0 -o wg0 -j ACCEPT
PostUp = iptables -A FORWARD -i wg0 -o wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -s 10.100.0.0/30 -o eth0 -j MASQUERADE
Cleanup rules on shutdown
PreDown = ip route del 192.168.2.0/24 dev wg0 2>/dev/null || true
PreDown = iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT 2>/dev/null || true
PreDown = iptables -D FORWARD -i eth0 -o wg0 -j ACCEPT 2>/dev/null || true
PreDown = iptables -D FORWARD -i wg0 -o wg0 -j ACCEPT 2>/dev/null || true
PreDown = iptables -t nat -D POSTROUTING -s 10.100.0.0/30 -o eth0 -j MASQUERADE 2>/dev/null || true
[Peer]
PublicKey = SITE_B_PUBLIC_KEY_HERE
AllowedIPs = 10.100.0.2/32, 192.168.2.0/24
PersistentKeepalive = 25
Configure secondary site WireGuard server
Configure the secondary site (Site B) with network 192.168.2.0/24. This server connects to the primary site.
[Interface]
PrivateKey = SITE_B_PRIVATE_KEY_HERE
Address = 10.100.0.2/30
SaveConfig = false
Enable IP forwarding for this interface
PreUp = sysctl -w net.ipv4.ip_forward=1
Add route to Site A's network
PostUp = ip route add 192.168.1.0/24 dev wg0
Configure firewall rules for forwarding
PostUp = iptables -A FORWARD -i wg0 -o eth0 -j ACCEPT
PostUp = iptables -A FORWARD -i eth0 -o wg0 -j ACCEPT
PostUp = iptables -A FORWARD -i wg0 -o wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -s 10.100.0.0/30 -o eth0 -j MASQUERADE
Cleanup rules on shutdown
PreDown = ip route del 192.168.1.0/24 dev wg0 2>/dev/null || true
PreDown = iptables -D FORWARD -i wg0 -o eth0 -j ACCEPT 2>/dev/null || true
PreDown = iptables -D FORWARD -i eth0 -o wg0 -j ACCEPT 2>/dev/null || true
PreDown = iptables -D FORWARD -i wg0 -o wg0 -j ACCEPT 2>/dev/null || true
PreDown = iptables -t nat -D POSTROUTING -s 10.100.0.0/30 -o eth0 -j MASQUERADE 2>/dev/null || true
[Peer]
PublicKey = SITE_A_PUBLIC_KEY_HERE
AllowedIPs = 10.100.0.1/32, 192.168.1.0/24
Endpoint = SITE_A_PUBLIC_IP:51820
PersistentKeepalive = 25
Enable permanent IP forwarding
Configure the kernel to forward IP packets between networks permanently. This setting persists across reboots.
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
Apply the changes immediately:
sudo sysctl -p /etc/sysctl.d/99-wireguard.conf
Set up advanced routing and firewall rules
Configure additional routing rules for better traffic control and security. Create a routing table specifically for WireGuard traffic.
echo "200 wireguard" | sudo tee -a /etc/iproute2/rt_tables
Add advanced routing rules to both sites:
#!/bin/bash
Advanced routing script for WireGuard site-to-site
Create custom routing table rules
ip rule add from 10.100.0.0/30 table wireguard priority 100
ip route add 10.100.0.0/30 dev wg0 table wireguard
ip route add 192.168.1.0/24 dev wg0 table wireguard
ip route add 192.168.2.0/24 dev wg0 table wireguard
Configure firewall rules for enhanced security
iptables -A INPUT -i wg0 -j ACCEPT
iptables -A OUTPUT -o wg0 -j ACCEPT
Rate limiting for VPN traffic
iptables -A FORWARD -i wg0 -m limit --limit 1000/sec --limit-burst 2000 -j ACCEPT
iptables -A FORWARD -i wg0 -j DROP
Log dropped packets for monitoring
iptables -A FORWARD -j LOG --log-prefix "WG-FORWARD-DROP: " --log-level 4
sudo chmod +x /etc/wireguard/routing.sh
Configure client network routing
Set up routing on client machines in each network to use the WireGuard gateway for remote network access. Add these routes on client machines or configure them via DHCP.
# On Site A clients (to reach Site B network)
sudo ip route add 192.168.2.0/24 via 192.168.1.1
On Site B clients (to reach Site A network)
sudo ip route add 192.168.1.0/24 via 192.168.2.1
To make these routes permanent, add them to network configuration:
network:
version: 2
ethernets:
eth0:
dhcp4: true
routes:
- to: 192.168.2.0/24
via: 192.168.1.1
Start and enable WireGuard services
Start the WireGuard interface on both sites and enable automatic startup.
sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0
sudo systemctl status wg-quick@wg0
Configure firewall ports
Open the required ports in your firewall for WireGuard communication.
sudo ufw allow 51820/udp
sudo ufw allow from 192.168.1.0/24 to 192.168.2.0/24
sudo ufw allow from 192.168.2.0/24 to 192.168.1.0/24
sudo ufw reload
Monitor and troubleshoot site-to-site connections
Set up connection monitoring
Create monitoring scripts to track tunnel health and automatically restart failed connections.
#!/bin/bash
WireGuard site-to-site monitoring script
LOGFILE="/var/log/wireguard-monitor.log"
REMOTE_IP="10.100.0.2" # Change based on site
REMOTE_NETWORK="192.168.2.1" # Gateway of remote network
Function to log messages
log_message() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> $LOGFILE
}
Check tunnel connectivity
ping -c 3 -W 5 $REMOTE_IP > /dev/null 2>&1
if [ $? -ne 0 ]; then
log_message "Tunnel connectivity failed, checking interface"
# Check if WireGuard interface is up
if ! ip link show wg0 > /dev/null 2>&1; then
log_message "WireGuard interface down, restarting"
systemctl restart wg-quick@wg0
sleep 10
fi
# Test remote network connectivity
ping -c 1 -W 5 $REMOTE_NETWORK > /dev/null 2>&1
if [ $? -eq 0 ]; then
log_message "Remote network reachable"
else
log_message "Remote network unreachable"
fi
else
log_message "Tunnel connectivity OK"
fi
Log connection statistics
wg show wg0 >> $LOGFILE
sudo chmod +x /etc/wireguard/monitor.sh
Set up automated monitoring
Configure a systemd timer to run the monitoring script every 5 minutes.
[Unit]
Description=WireGuard Site-to-Site Monitor
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
ExecStart=/etc/wireguard/monitor.sh
User=root
[Unit]
Description=Run WireGuard Monitor every 5 minutes
Requires=wireguard-monitor.service
[Timer]
OnCalendar=*:0/5
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now wireguard-monitor.timer
sudo systemctl status wireguard-monitor.timer
Configure traffic analysis and logging
Set up detailed logging for traffic analysis and troubleshooting.
# WireGuard logging configuration
:msg, contains, "WG-" /var/log/wireguard.log
& stop
sudo systemctl restart rsyslog
sudo logrotate -d /etc/logrotate.d/wireguard
/var/log/wireguard.log {
daily
rotate 30
compress
delaycompress
missingok
notifempty
create 644 syslog syslog
postrotate
systemctl reload rsyslog
endscript
}
Verify your setup
Test the site-to-site connection from both locations to ensure full network connectivity.
# Check WireGuard interface status
sudo wg show
ip addr show wg0
Test tunnel connectivity between gateways
ping -c 4 10.100.0.2 # From Site A to Site B
ping -c 4 10.100.0.1 # From Site B to Site A
Test network-to-network connectivity
ping -c 4 192.168.2.1 # From Site A to Site B gateway
ping -c 4 192.168.1.1 # From Site B to Site A gateway
Check routing table
ip route show table wireguard
Monitor real-time traffic
sudo tcpdump -i wg0 -n
Check firewall rules
sudo iptables -L FORWARD -n -v
Verify monitoring logs
sudo tail -f /var/log/wireguard-monitor.log
iperf3 to test bandwidth between sites. Install on both networks and run iperf3 -s on one side and iperf3 -c remote_ip on the other.Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Tunnel establishes but no network traffic | IP forwarding disabled or missing routes | sudo sysctl -w net.ipv4.ip_forward=1 and check routing table |
| Connection fails after firewall restart | Iptables rules not persistent | Save rules with sudo iptables-save > /etc/iptables/rules.v4 |
| One-way connectivity only | Missing return route on client networks | Add static routes on client machines or DHCP server |
| High latency or packet loss | MTU size issues | Set MTU to 1420: ip link set mtu 1420 dev wg0 |
| Frequent connection drops | NAT timeout or keepalive issues | Reduce PersistentKeepalive to 15 seconds |
| DNS resolution fails across sites | DNS server not accessible from remote network | Configure DNS forwarding or add remote DNS servers |
Next steps
- Monitor WireGuard VPN server with Prometheus and Grafana dashboards
- Configure WireGuard VPN with DNS filtering and ad blocking using Pi-hole and Unbound
- Configure HAProxy multi-site SSL termination with SNI for secure load balancing
- Set up WireGuard high availability cluster with automatic failover
- Configure WireGuard mesh network for multiple sites with dynamic routing
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Global variables
SITE_ROLE=""
SITE_A_ENDPOINT=""
SITE_B_ENDPOINT=""
SITE_A_NETWORK="192.168.1.0/24"
SITE_B_NETWORK="192.168.2.0/24"
WG_NETWORK="10.100.0.0/30"
WG_PORT="51820"
# Cleanup function
cleanup() {
echo -e "${RED}[ERROR] Installation failed. Cleaning up...${NC}"
systemctl stop wg-quick@wg0 2>/dev/null || true
systemctl disable wg-quick@wg0 2>/dev/null || true
rm -f /etc/wireguard/wg0.conf
rm -f /etc/wireguard/private.key /etc/wireguard/public.key
}
trap cleanup ERR
usage() {
echo "Usage: $0 <site_role> [site_a_endpoint] [site_b_endpoint]"
echo " site_role: 'primary' or 'secondary'"
echo " site_a_endpoint: Public IP/hostname of primary site (required for secondary)"
echo " site_b_endpoint: Public IP/hostname of secondary site (optional)"
echo ""
echo "Examples:"
echo " $0 primary"
echo " $0 secondary 203.0.113.10"
echo " $0 primary 203.0.113.10 203.0.113.20"
exit 1
}
log() {
echo -e "${GREEN}$1${NC}"
}
warn() {
echo -e "${YELLOW}$1${NC}"
}
error() {
echo -e "${RED}$1${NC}"
exit 1
}
check_root() {
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root"
fi
}
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update"
PKG_INSTALL="apt install -y"
IPTABLES_SERVICE="netfilter-persistent"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
IPTABLES_SERVICE="iptables"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
IPTABLES_SERVICE="iptables"
;;
*)
error "Unsupported distribution: $ID"
;;
esac
else
error "Cannot detect distribution (no /etc/os-release found)"
fi
log "[✓] Detected distribution: $ID"
}
install_packages() {
echo "[2/8] Installing WireGuard and dependencies..."
$PKG_UPDATE
case "$PKG_MGR" in
apt)
$PKG_INSTALL wireguard wireguard-tools iptables-persistent
;;
dnf|yum)
$PKG_INSTALL wireguard-tools iptables-services
systemctl enable --now iptables
;;
esac
log "[✓] Packages installed successfully"
}
enable_ip_forwarding() {
echo "[3/8] Enabling IP forwarding..."
echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-wireguard.conf
sysctl -p /etc/sysctl.d/99-wireguard.conf
log "[✓] IP forwarding enabled"
}
generate_keys() {
echo "[4/8] Generating WireGuard keys..."
mkdir -p /etc/wireguard
chmod 700 /etc/wireguard
wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key
chmod 600 /etc/wireguard/private.key
chmod 644 /etc/wireguard/public.key
log "[✓] Keys generated successfully"
}
get_default_interface() {
ip route | grep default | awk '{print $5}' | head -n1
}
create_config() {
echo "[5/8] Creating WireGuard configuration..."
local private_key=$(cat /etc/wireguard/private.key)
local default_iface=$(get_default_interface)
if [[ "$SITE_ROLE" == "primary" ]]; then
cat > /etc/wireguard/wg0.conf << EOF
[Interface]
PrivateKey = $private_key
Address = 10.100.0.1/30
ListenPort = $WG_PORT
SaveConfig = false
# Enable IP forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1
# Add route to remote network
PostUp = ip route add $SITE_B_NETWORK dev wg0
# Configure firewall rules
PostUp = iptables -A FORWARD -i wg0 -o $default_iface -j ACCEPT
PostUp = iptables -A FORWARD -i $default_iface -o wg0 -j ACCEPT
PostUp = iptables -A FORWARD -i wg0 -o wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -s $WG_NETWORK -o $default_iface -j MASQUERADE
# Cleanup on shutdown
PreDown = ip route del $SITE_B_NETWORK dev wg0 2>/dev/null || true
PreDown = iptables -D FORWARD -i wg0 -o $default_iface -j ACCEPT 2>/dev/null || true
PreDown = iptables -D FORWARD -i $default_iface -o wg0 -j ACCEPT 2>/dev/null || true
PreDown = iptables -D FORWARD -i wg0 -o wg0 -j ACCEPT 2>/dev/null || true
PreDown = iptables -t nat -D POSTROUTING -s $WG_NETWORK -o $default_iface -j MASQUERADE 2>/dev/null || true
# Peer configuration will be added manually
EOF
else
if [[ -z "$SITE_A_ENDPOINT" ]]; then
error "Primary site endpoint is required for secondary site"
fi
cat > /etc/wireguard/wg0.conf << EOF
[Interface]
PrivateKey = $private_key
Address = 10.100.0.2/30
SaveConfig = false
# Enable IP forwarding
PreUp = sysctl -w net.ipv4.ip_forward=1
# Add route to remote network
PostUp = ip route add $SITE_A_NETWORK dev wg0
# Configure firewall rules
PostUp = iptables -A FORWARD -i wg0 -o $default_iface -j ACCEPT
PostUp = iptables -A FORWARD -i $default_iface -o wg0 -j ACCEPT
PostUp = iptables -A FORWARD -i wg0 -o wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -s $WG_NETWORK -o $default_iface -j MASQUERADE
# Cleanup on shutdown
PreDown = ip route del $SITE_A_NETWORK dev wg0 2>/dev/null || true
PreDown = iptables -D FORWARD -i wg0 -o $default_iface -j ACCEPT 2>/dev/null || true
PreDown = iptables -D FORWARD -i $default_iface -o wg0 -j ACCEPT 2>/dev/null || true
PreDown = iptables -D FORWARD -i wg0 -o wg0 -j ACCEPT 2>/dev/null || true
PreDown = iptables -t nat -D POSTROUTING -s $WG_NETWORK -o $default_iface -j MASQUERADE 2>/dev/null || true
[Peer]
Endpoint = $SITE_A_ENDPOINT:$WG_PORT
PublicKey = PLACEHOLDER_PRIMARY_PUBLIC_KEY
AllowedIPs = 10.100.0.1/32, $SITE_A_NETWORK
PersistentKeepalive = 25
EOF
fi
chmod 600 /etc/wireguard/wg0.conf
log "[✓] Configuration created"
}
configure_selinux() {
if command -v getenforce >/dev/null 2>&1 && [[ "$(getenforce)" != "Disabled" ]]; then
echo "[6/8] Configuring SELinux..."
setsebool -P domain_can_mmap_files 1 2>/dev/null || true
log "[✓] SELinux configured"
else
echo "[6/8] SELinux not active, skipping..."
fi
}
enable_service() {
echo "[7/8] Enabling WireGuard service..."
systemctl enable wg-quick@wg0
warn "Service enabled but not started yet - peer configuration needed first"
log "[✓] Service configured"
}
show_summary() {
echo "[8/8] Installation complete!"
echo ""
echo -e "${BLUE}=== WireGuard Site-to-Site VPN Configuration ===${NC}"
echo -e "${BLUE}Site Role:${NC} $SITE_ROLE"
echo -e "${BLUE}Public Key:${NC} $(cat /etc/wireguard/public.key)"
echo -e "${BLUE}Network:${NC} $(if [[ "$SITE_ROLE" == "primary" ]]; then echo "$SITE_A_NETWORK"; else echo "$SITE_B_NETWORK"; fi)"
echo -e "${BLUE}Tunnel IP:${NC} $(if [[ "$SITE_ROLE" == "primary" ]]; then echo "10.100.0.1"; else echo "10.100.0.2"; fi)"
echo ""
if [[ "$SITE_ROLE" == "primary" ]]; then
echo -e "${YELLOW}Next steps for PRIMARY site:${NC}"
echo "1. Share your public key with the secondary site"
echo "2. Get the secondary site's public key"
echo "3. Add peer configuration:"
echo " wg set wg0 peer SECONDARY_PUBLIC_KEY allowed-ips 10.100.0.2/32,$SITE_B_NETWORK"
echo "4. Start the service: systemctl start wg-quick@wg0"
else
echo -e "${YELLOW}Next steps for SECONDARY site:${NC}"
echo "1. Replace PLACEHOLDER_PRIMARY_PUBLIC_KEY in /etc/wireguard/wg0.conf"
echo "2. Start the service: systemctl start wg-quick@wg0"
echo "3. Share your public key with the primary site"
fi
echo ""
echo -e "${GREEN}Installation completed successfully!${NC}"
}
# Main execution
main() {
if [[ $# -lt 1 ]]; then
usage
fi
SITE_ROLE="$1"
if [[ "$SITE_ROLE" != "primary" && "$SITE_ROLE" != "secondary" ]]; then
error "Invalid site role. Must be 'primary' or 'secondary'"
fi
if [[ "$SITE_ROLE" == "secondary" && $# -lt 2 ]]; then
error "Secondary site requires primary site endpoint"
fi
if [[ $# -ge 2 ]]; then
SITE_A_ENDPOINT="$2"
fi
if [[ $# -ge 3 ]]; then
SITE_B_ENDPOINT="$3"
fi
echo -e "${BLUE}WireGuard Site-to-Site VPN Installer${NC}"
echo "===================================="
echo "[1/8] Checking prerequisites..."
check_root
detect_distro
install_packages
enable_ip_forwarding
generate_keys
create_config
configure_selinux
enable_service
show_summary
}
main "$@"
Review the script before running. Execute with: bash install.sh