Configure a resilient Tailscale mesh VPN with multiple exit nodes for high availability site-to-site connectivity. Set up subnet routing, automatic failover, and monitoring across distributed networks.
Prerequisites
- Multiple Linux servers with root access
- Tailscale account
- Public IP addresses on exit nodes
- Basic understanding of networking concepts
What this solves
Tailscale site-to-site VPN with multiple exit nodes provides redundant connectivity between different network segments without single points of failure. This setup automatically routes traffic through available exit nodes when primary connections fail, ensuring continuous network access for distributed teams and infrastructure.
Step-by-step configuration
Install Tailscale on all nodes
Install Tailscale on every server that will participate in the mesh network. We'll install on at least 3 nodes for proper redundancy.
curl -fsSL https://tailscale.com/install.sh | sh
Configure IP forwarding on exit nodes
Enable IP forwarding on servers that will act as exit nodes to route traffic between network segments.
echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
Authenticate and start Tailscale on primary exit node
Connect the first exit node and advertise subnet routes. This node will handle traffic for the local network segment.
sudo tailscale up --advertise-routes=192.168.1.0/24 --advertise-exit-node --hostname=exit-node-1
Configure secondary exit node
Set up the second exit node on a different network segment or geographic location for redundancy.
sudo tailscale up --advertise-routes=192.168.2.0/24 --advertise-exit-node --hostname=exit-node-2
Add tertiary exit node
Configure a third exit node to provide additional redundancy and load distribution.
sudo tailscale up --advertise-routes=192.168.3.0/24 --advertise-exit-node --hostname=exit-node-3
Configure client nodes
Connect client machines that will use the exit nodes for network access.
sudo tailscale up --hostname=client-node-1
Enable subnet routes in Tailscale admin
Approve advertised routes and exit nodes through the Tailscale admin console at https://login.tailscale.com/admin/machines.
tailscale status
tailscale ip -4
Configure route priorities
Set route priorities to control traffic flow and failover behavior using Tailscale ACL policies.
{
"acls": [
{
"action": "accept",
"src": ["*"],
"dst": [":"]
}
],
"autoApprovers": {
"routes": {
"192.168.1.0/24": ["exit-node-1"],
"192.168.2.0/24": ["exit-node-2"],
"192.168.3.0/24": ["exit-node-3"]
},
"exitNode": ["exit-node-1", "exit-node-2", "exit-node-3"]
}
}
Configure automatic failover
Create a health check script that monitors exit node availability and switches routes automatically.
#!/bin/bash
PRIMARY_EXIT="100.64.1.10"
SECONDARY_EXIT="100.64.1.11"
TERTIARY_EXIT="100.64.1.12"
check_exit_node() {
local exit_ip="$1"
ping -c 3 -W 5 "$exit_ip" > /dev/null 2>&1
return $?
}
switch_exit_node() {
local new_exit="$1"
sudo tailscale set --exit-node="$new_exit"
logger "Tailscale: Switched to exit node $new_exit"
}
if check_exit_node "$PRIMARY_EXIT"; then
switch_exit_node "$PRIMARY_EXIT"
elif check_exit_node "$SECONDARY_EXIT"; then
switch_exit_node "$SECONDARY_EXIT"
elif check_exit_node "$TERTIARY_EXIT"; then
switch_exit_node "$TERTIARY_EXIT"
else
logger "Tailscale: All exit nodes unreachable"
fi
Make failover script executable
Set proper permissions and create a systemd timer for automated failover checks.
sudo chmod +x /usr/local/bin/tailscale-failover.sh
sudo chown root:root /usr/local/bin/tailscale-failover.sh
Create systemd service for failover
Configure systemd to run the failover script as a service.
[Unit]
Description=Tailscale Exit Node Failover
After=tailscaled.service
Requires=tailscaled.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/tailscale-failover.sh
User=root
[Install]
WantedBy=multi-user.target
Create systemd timer for periodic checks
Set up automatic failover checks every 30 seconds to ensure rapid detection of node failures.
[Unit]
Description=Run Tailscale failover check every 30 seconds
Requires=tailscale-failover.service
[Timer]
OnBootSec=30
OnUnitActiveSec=30
Unit=tailscale-failover.service
[Install]
WantedBy=timers.target
Enable failover monitoring
Start and enable the failover timer to begin automatic monitoring and switching.
sudo systemctl daemon-reload
sudo systemctl enable --now tailscale-failover.timer
sudo systemctl status tailscale-failover.timer
Configure monitoring alerts
Set up monitoring to track exit node health and failover events using system logs.
# Log Tailscale events to separate file
if $programname == 'tailscale-failover' then /var/log/tailscale-failover.log
& stop
sudo systemctl restart rsyslog
Test failover functionality
Verify that failover works by stopping one exit node and checking if traffic switches automatically.
sudo tailscale status
sudo systemctl stop tailscaled # On primary exit node
tail -f /var/log/tailscale-failover.log # On client
Configure advanced routing policies
Set up load balancing
Configure multiple exit nodes to share traffic load using Tailscale's built-in load balancing features.
tailscale set --exit-node-allow-lan-access=true
tailscale set --shields-up=false
Configure subnet-specific routing
Route different subnets through specific exit nodes based on geographic or performance requirements.
#!/bin/bash
Route management subnet through primary exit
sudo ip route add 10.0.1.0/24 via 100.64.1.10
Route production subnet through secondary exit
sudo ip route add 10.0.2.0/24 via 100.64.1.11
Route development subnet through tertiary exit
sudo ip route add 10.0.3.0/24 via 100.64.1.12
Verify your setup
Check that your Tailscale mesh network is functioning correctly with multiple exit nodes.
tailscale status
tailscale netcheck
ping 100.64.1.10
ping 100.64.1.11
ping 100.64.1.12
sudo systemctl status tailscale-failover.timer
journalctl -u tailscale-failover.service -f
Monitor your mesh network
Set up comprehensive monitoring for your Tailscale deployment to track performance and availability. You can integrate with existing monitoring solutions like the Tailscale monitoring with Prometheus and Grafana setup for detailed metrics and alerting.
Create health check endpoint
Set up a simple HTTP endpoint to monitor exit node health externally.
#!/usr/bin/env python3
import subprocess
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
class HealthHandler(BaseHTTPRequestHandler):
def do_GET(self):
try:
result = subprocess.run(['tailscale', 'status', '--json'],
capture_output=True, text=True)
status = json.loads(result.stdout)
if status.get('BackendState') == 'Running':
self.send_response(200)
self.end_headers()
self.wfile.write(b'Tailscale healthy')
else:
self.send_response(503)
self.end_headers()
self.wfile.write(b'Tailscale unhealthy')
except Exception as e:
self.send_response(500)
self.end_headers()
self.wfile.write(f'Error: {str(e)}'.encode())
httpd = HTTPServer(('0.0.0.0', 8080), HealthHandler)
httpd.serve_forever()
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Subnet routes not working | Routes not approved in admin console | Approve routes at https://login.tailscale.com/admin/machines |
| Exit node not accessible | IP forwarding disabled | sudo sysctl -w net.ipv4.ip_forward=1 |
| Failover not triggering | Timer service not running | sudo systemctl start tailscale-failover.timer |
| High latency between sites | Suboptimal exit node selection | Manually set preferred exit node: tailscale set --exit-node=NODE_IP |
| Authentication failures | Expired auth tokens | sudo tailscale login to re-authenticate |
| DNS resolution issues | Split DNS not configured | tailscale set --accept-dns=true |
Security considerations
Implement additional security measures for production Tailscale deployments to protect your mesh network.
Configure ACL policies
Implement network segmentation using Tailscale ACL policies to control traffic flow between different network zones.
{
"groups": {
"group:admins": ["user1@example.com"],
"group:developers": ["user2@example.com", "user3@example.com"],
"group:servers": ["tag:server"]
},
"acls": [
{
"action": "accept",
"src": ["group:admins"],
"dst": [":"]
},
{
"action": "accept",
"src": ["group:developers"],
"dst": ["tag:server:22,80,443"]
}
]
}
Next steps
- Configure Tailscale monitoring with Prometheus and Grafana for comprehensive metrics
- Implement Tailscale OAuth integration with enterprise identity providers
- Configure Tailscale with Kubernetes for container networking
- Set up Tailscale DNS filtering with DoH and ad blocking
- Implement zero trust access policies with device compliance
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Tailscale Site-to-Site VPN with Multiple Exit Nodes Setup Script
# Supports Ubuntu, Debian, AlmaLinux, Rocky Linux, CentOS, RHEL, and Fedora
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Default values
NODE_TYPE=""
SUBNET=""
HOSTNAME=""
EXIT_NODE_IPS=""
# Usage message
usage() {
echo "Usage: $0 --type [exit|client] [--subnet SUBNET] [--hostname HOSTNAME] [--exit-ips IP1,IP2,IP3]"
echo " --type: Node type (exit or client)"
echo " --subnet: Network subnet to advertise (required for exit nodes)"
echo " --hostname: Tailscale hostname (optional)"
echo " --exit-ips: Comma-separated list of exit node IPs for failover (client nodes only)"
echo ""
echo "Examples:"
echo " $0 --type exit --subnet 192.168.1.0/24 --hostname exit-node-1"
echo " $0 --type client --hostname client-1 --exit-ips 100.64.1.10,100.64.1.11,100.64.1.12"
exit 1
}
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--type)
NODE_TYPE="$2"
shift 2
;;
--subnet)
SUBNET="$2"
shift 2
;;
--hostname)
HOSTNAME="$2"
shift 2
;;
--exit-ips)
EXIT_NODE_IPS="$2"
shift 2
;;
-h|--help)
usage
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
usage
;;
esac
done
# Validate required arguments
if [[ -z "$NODE_TYPE" ]]; then
echo -e "${RED}Error: --type is required${NC}"
usage
fi
if [[ "$NODE_TYPE" == "exit" && -z "$SUBNET" ]]; then
echo -e "${RED}Error: --subnet is required for exit nodes${NC}"
usage
fi
# Error handling
cleanup() {
echo -e "${RED}Error occurred. Cleaning up...${NC}"
systemctl stop tailscaled 2>/dev/null || true
}
trap cleanup ERR
# Check if running as root
if [[ $EUID -ne 0 ]]; then
echo -e "${RED}This script must be run as root${NC}"
exit 1
fi
echo -e "${GREEN}Starting Tailscale site-to-site VPN setup...${NC}"
# Detect distribution and set package manager
echo -e "${YELLOW}[1/8] Detecting Linux distribution...${NC}"
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"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
# Fallback to yum for older versions
if ! command -v dnf &> /dev/null; then
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
fi
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
;;
*)
echo -e "${RED}Unsupported distribution: $ID${NC}"
exit 1
;;
esac
echo -e "${GREEN}Detected: $PRETTY_NAME${NC}"
else
echo -e "${RED}Cannot detect Linux distribution${NC}"
exit 1
fi
# Update package lists
echo -e "${YELLOW}[2/8] Updating package lists...${NC}"
$PKG_UPDATE
# Install required packages
echo -e "${YELLOW}[3/8] Installing required packages...${NC}"
$PKG_INSTALL curl wget
# Install Tailscale
echo -e "${YELLOW}[4/8] Installing Tailscale...${NC}"
curl -fsSL https://tailscale.com/install.sh | sh
# Configure IP forwarding for exit nodes
if [[ "$NODE_TYPE" == "exit" ]]; then
echo -e "${YELLOW}[5/8] Configuring IP forwarding for exit node...${NC}"
# Enable IP forwarding
if ! grep -q "net.ipv4.ip_forward = 1" /etc/sysctl.conf; then
echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
fi
if ! grep -q "net.ipv6.conf.all.forwarding = 1" /etc/sysctl.conf; then
echo 'net.ipv6.conf.all.forwarding = 1' >> /etc/sysctl.conf
fi
sysctl -p
# Configure firewall based on distribution
if command -v firewall-cmd &> /dev/null; then
# RHEL-based systems with firewalld
systemctl enable firewalld
systemctl start firewalld
firewall-cmd --permanent --add-masquerade
firewall-cmd --reload
elif command -v ufw &> /dev/null; then
# Ubuntu/Debian with ufw
ufw --force enable
ufw allow in on tailscale0
ufw allow out on tailscale0
fi
else
echo -e "${YELLOW}[5/8] Skipping IP forwarding (client node)...${NC}"
fi
# Start and enable Tailscale service
echo -e "${YELLOW}[6/8] Starting Tailscale service...${NC}"
systemctl enable tailscaled
systemctl start tailscaled
# Configure Tailscale based on node type
echo -e "${YELLOW}[7/8] Configuring Tailscale...${NC}"
if [[ "$NODE_TYPE" == "exit" ]]; then
# Configure exit node
TAILSCALE_CMD="tailscale up --advertise-routes=$SUBNET --advertise-exit-node"
if [[ -n "$HOSTNAME" ]]; then
TAILSCALE_CMD="$TAILSCALE_CMD --hostname=$HOSTNAME"
fi
echo -e "${GREEN}Starting Tailscale as exit node...${NC}"
echo "Please authenticate using the URL that will be displayed:"
$TAILSCALE_CMD
# Create failover monitoring script for exit nodes
cat > /usr/local/bin/tailscale-health-check.sh << 'EOF'
#!/bin/bash
# Health check script for Tailscale exit nodes
LOGFILE="/var/log/tailscale-health.log"
STATUS=$(tailscale status --json 2>/dev/null)
if [[ $? -eq 0 ]]; then
echo "$(date): Tailscale healthy" >> "$LOGFILE"
else
echo "$(date): Tailscale unhealthy, restarting..." >> "$LOGFILE"
systemctl restart tailscaled
fi
EOF
chmod 755 /usr/local/bin/tailscale-health-check.sh
chown root:root /usr/local/bin/tailscale-health-check.sh
else
# Configure client node
TAILSCALE_CMD="tailscale up"
if [[ -n "$HOSTNAME" ]]; then
TAILSCALE_CMD="$TAILSCALE_CMD --hostname=$HOSTNAME"
fi
echo -e "${GREEN}Starting Tailscale as client node...${NC}"
echo "Please authenticate using the URL that will be displayed:"
$TAILSCALE_CMD
# Create failover script for client nodes if exit IPs provided
if [[ -n "$EXIT_NODE_IPS" ]]; then
IFS=',' read -ra EXIT_ARRAY <<< "$EXIT_NODE_IPS"
cat > /usr/local/bin/tailscale-failover.sh << EOF
#!/bin/bash
# Tailscale exit node failover script
LOGFILE="/var/log/tailscale-failover.log"
EXIT_NODES=(${EXIT_ARRAY[@]})
check_exit_node() {
local exit_ip="\$1"
ping -c 3 -W 5 "\$exit_ip" > /dev/null 2>&1
return \$?
}
switch_exit_node() {
local new_exit="\$1"
tailscale set --exit-node="\$new_exit"
echo "\$(date): Switched to exit node \$new_exit" >> "\$LOGFILE"
}
for exit_node in "\${EXIT_NODES[@]}"; do
if check_exit_node "\$exit_node"; then
switch_exit_node "\$exit_node"
exit 0
fi
done
echo "\$(date): All exit nodes unreachable" >> "\$LOGFILE"
EOF
chmod 755 /usr/local/bin/tailscale-failover.sh
chown root:root /usr/local/bin/tailscale-failover.sh
# Create systemd timer for failover checks
cat > /etc/systemd/system/tailscale-failover.service << 'EOF'
[Unit]
Description=Tailscale Exit Node Failover
After=tailscaled.service
Requires=tailscaled.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/tailscale-failover.sh
User=root
EOF
cat > /etc/systemd/system/tailscale-failover.timer << 'EOF'
[Unit]
Description=Run Tailscale failover check every 30 seconds
Requires=tailscale-failover.service
[Timer]
OnCalendar=*:*:0/30
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl daemon-reload
systemctl enable tailscale-failover.timer
systemctl start tailscale-failover.timer
fi
fi
# Verification
echo -e "${YELLOW}[8/8] Verifying installation...${NC}"
sleep 5
if systemctl is-active --quiet tailscaled; then
echo -e "${GREEN}✓ Tailscaled service is running${NC}"
else
echo -e "${RED}✗ Tailscaled service is not running${NC}"
exit 1
fi
if tailscale status > /dev/null 2>&1; then
echo -e "${GREEN}✓ Tailscale is connected${NC}"
echo -e "${GREEN}Tailscale Status:${NC}"
tailscale status
echo -e "${GREEN}Tailscale IP:${NC}"
tailscale ip -4
else
echo -e "${RED}✗ Tailscale is not properly configured${NC}"
exit 1
fi
echo -e "${GREEN}Installation completed successfully!${NC}"
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Go to https://login.tailscale.com/admin/machines to approve routes and exit nodes"
echo "2. Configure ACL policies for route priorities if needed"
if [[ "$NODE_TYPE" == "exit" ]]; then
echo "3. Monitor exit node health with: tail -f /var/log/tailscale-health.log"
else
echo "3. Monitor failover status with: tail -f /var/log/tailscale-failover.log"
fi
Review the script before running. Execute with: bash install.sh