Set up a zero-trust mesh VPN network with Tailscale that includes subnet routing for local network access, granular ACL policies for security, and exit nodes for secure internet access.
Prerequisites
- Root or sudo access
- Internet connectivity
- Tailscale account (free tier available)
- Local network subnet knowledge
What this solves
Tailscale creates a secure mesh VPN network that connects devices across the internet without complex firewall configurations or port forwarding. This tutorial shows you how to implement subnet routing to access remote local networks, configure ACL policies for zero-trust security, and set up exit nodes for secure internet browsing through your network.
Step-by-step configuration
Update system packages
Start by updating your package manager to ensure you get the latest versions.
sudo apt update && sudo apt upgrade -y
Install Tailscale client
Install the official Tailscale package from their repository to ensure you get automatic updates and the latest security patches.
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/$(lsb_release -cs).noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/$(lsb_release -cs).tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list
sudo apt update
sudo apt install -y tailscale
Enable and start Tailscale service
Enable the Tailscale daemon to start automatically on boot and verify it's running properly.
sudo systemctl enable --now tailscaled
sudo systemctl status tailscaled
Authenticate with Tailscale
Connect this device to your Tailscale network by authenticating through the web interface. This command generates a unique authentication URL.
sudo tailscale up
Open the provided URL in your web browser and follow the authentication process. If you don't have a Tailscale account, create one during this step.
Configure subnet routing
Enable this device to route traffic for your local subnet, allowing other Tailscale devices to access local network resources like printers, NAS devices, or internal services.
sudo tailscale up --advertise-routes=192.168.1.0/24
Replace 192.168.1.0/24 with your actual local network subnet. For multiple subnets, use comma separation like --advertise-routes=192.168.1.0/24,10.0.0.0/8.
Enable IP forwarding
Configure the kernel to forward packets between network interfaces, which is required for subnet routing to function properly.
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
Configure exit node capability
Enable this device to act as an exit node, allowing other Tailscale devices to route all their internet traffic through this connection for enhanced privacy or to access geo-restricted content.
sudo tailscale up --advertise-exit-node
Create ACL policy file
Access your Tailscale admin console at https://login.tailscale.com/admin/acls to configure access control policies. Replace the default policy with this zero-trust configuration.
{
"tagOwners": {
"tag:server": ["autogroup:admin"],
"tag:client": ["autogroup:admin"],
"tag:printer": ["autogroup:admin"]
},
"groups": {
"group:developers": ["user1@example.com", "user2@example.com"],
"group:admins": ["admin@example.com"]
},
"acls": [
{
"action": "accept",
"src": ["group:admins"],
"dst": [":"]
},
{
"action": "accept",
"src": ["group:developers"],
"dst": ["tag:server:22,80,443,8080"]
},
{
"action": "accept",
"src": ["tag:client"],
"dst": ["tag:server:80,443"]
},
{
"action": "accept",
"src": ["autogroup:members"],
"dst": ["tag:printer:631,9100"]
}
],
"ssh": [
{
"action": "accept",
"src": ["group:admins"],
"dst": ["tag:server"],
"users": ["root", "autogroup:nonroot"]
}
]
}
Apply device tags
Tag devices in your Tailscale admin console to apply the ACL policies. Navigate to the Machines tab and edit each device to add appropriate tags like tag:server, tag:client, or tag:printer.
Configure custom DNS settings
Set up custom DNS resolution for your Tailscale network to resolve internal hostnames and improve performance.
sudo tailscale up --accept-dns=true
In the Tailscale admin console, go to DNS settings and configure:
- Global nameservers: 1.1.1.1, 8.8.8.8
- Split DNS: example.local -> 192.168.1.10
- MagicDNS: Enabled (allows using device names like server.tail-scale.ts.net)
Enable key expiry and disable key expiry
Configure authentication key settings for enhanced security. By default, device keys expire after 180 days.
sudo tailscale up --auth-key=tskey-auth-xxxxx-xxxxxxxxxxxxxxxxxxxx
For servers that need persistent connectivity, disable key expiry in the admin console by selecting the device and clicking "Disable key expiry." For client devices, leave expiry enabled for better security.
Verify your setup
Check that Tailscale is running and connected to your network with proper routing configuration.
sudo tailscale status
sudo tailscale ip -4
sudo tailscale netcheck
ip route show | grep tailscale
Test connectivity to other devices on your Tailscale network and verify subnet routing is working.
ping 100.x.x.x # Replace with another Tailscale device IP
ping 192.168.1.1 # Test subnet routing to local gateway
tailscale ping hostname.tail-scale.ts.net
Monitor and manage connections
Use these commands to monitor your Tailscale network and troubleshoot connectivity issues.
# View detailed status and connection info
sudo tailscale status --peers
Check network connectivity and NAT traversal
sudo tailscale netcheck
View logs for troubleshooting
sudo journalctl -u tailscaled -f
Test connection quality to specific peer
tailscale ping --verbose hostname.tail-scale.ts.net
View routing table
sudo tailscale status --json | jq '.Peer[] | {Name: .HostName, IP: .TailscaleIPs, Routes: .PrimaryRoutes}'
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Subnet routes not working | Routes not approved or IP forwarding disabled | Approve routes in admin console and verify sudo sysctl net.ipv4.ip_forward returns 1 |
| Can't connect to peers | Firewall blocking Tailscale or NAT issues | Run sudo tailscale netcheck and check firewall rules with sudo ufw status |
| Exit node not available | Exit node not approved or offline | Approve exit node in admin console and verify it's online with tailscale status |
| DNS resolution failing | MagicDNS disabled or DNS settings incorrect | Enable MagicDNS in admin console and run sudo tailscale up --accept-dns=true |
| ACL blocking connections | Restrictive ACL policy or missing tags | Check ACL syntax in admin console and verify device tags are applied correctly |
| Service won't start | Conflicting VPN or network configuration | Check for conflicts with sudo systemctl status tailscaled and review logs |
Security best practices
Implement these security measures to harden your Tailscale deployment for production use.
Regular ACL audits and user access reviews ensure your zero-trust policies remain effective. Consider implementing device approval workflows and enabling audit logging for compliance requirements.
# Enable HTTPS certificates for internal services
sudo tailscale cert example.tail-scale.ts.net
Use Tailscale SSH for secure access (requires ACL configuration)
tailscale ssh user@hostname.tail-scale.ts.net
Performance optimization
Optimize Tailscale performance for high-throughput scenarios and reduce latency between peers. These settings are particularly important when using subnet routing or exit nodes extensively.
# Enable direct connections and optimize for performance
sudo tailscale up --accept-routes --shields-up=false
Check for direct connections vs relayed
tailscale netcheck
For production deployments, consider implementing the network performance optimizations covered in our Linux network stack performance tuning guide.
Integration with existing infrastructure
Tailscale works alongside existing VPN solutions and can complement traditional network security tools. For environments with existing network monitoring, integrate Tailscale metrics with your monitoring stack.
When deploying in containerized environments, ensure proper network configuration as detailed in our process monitoring and resource management tutorial.
Next steps
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
SUBNET=""
ENABLE_EXIT_NODE=false
CLEANUP_FILES=()
# Usage function
usage() {
echo "Usage: $0 --subnet=<CIDR> [--enable-exit-node]"
echo "Example: $0 --subnet=192.168.1.0/24 --enable-exit-node"
echo "Options:"
echo " --subnet=<CIDR> Local subnet to advertise (required)"
echo " --enable-exit-node Enable exit node functionality (optional)"
echo " --help Show this help message"
exit 1
}
# Error handling and cleanup
cleanup() {
if [ ${#CLEANUP_FILES[@]} -gt 0 ]; then
echo -e "${YELLOW}[CLEANUP] Removing temporary files...${NC}"
for file in "${CLEANUP_FILES[@]}"; do
[ -f "$file" ] && rm -f "$file"
done
fi
}
error_exit() {
echo -e "${RED}[ERROR] $1${NC}" >&2
cleanup
exit 1
}
trap cleanup EXIT
trap 'error_exit "Script failed on line $LINENO"' ERR
# Parse arguments
for arg in "$@"; do
case $arg in
--subnet=*)
SUBNET="${arg#*=}"
shift
;;
--enable-exit-node)
ENABLE_EXIT_NODE=true
shift
;;
--help)
usage
;;
*)
echo -e "${RED}Unknown option: $arg${NC}"
usage
;;
esac
done
# Validate required arguments
if [ -z "$SUBNET" ]; then
echo -e "${RED}Error: --subnet parameter is required${NC}"
usage
fi
# Validate subnet format
if ! echo "$SUBNET" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$'; then
error_exit "Invalid subnet format. Use CIDR notation (e.g., 192.168.1.0/24)"
fi
echo -e "${BLUE}Tailscale Mesh VPN Installation Script${NC}"
echo "======================================"
# Check if running as root or with sudo
echo -e "${BLUE}[1/9] Checking privileges...${NC}"
if [ "$EUID" -ne 0 ]; then
error_exit "This script must be run as root or with sudo"
fi
# Auto-detect distribution
echo -e "${BLUE}[2/9] Detecting operating system...${NC}"
if [ ! -f /etc/os-release ]; then
error_exit "/etc/os-release not found. Cannot detect distribution."
fi
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_UPDATE="apt update && apt upgrade -y"
PKG_INSTALL="apt install -y"
DISTRO_FAMILY="debian"
;;
almalinux|rocky|centos|rhel|ol)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
DISTRO_FAMILY="rhel"
RHEL_VERSION="${VERSION_ID%%.*}"
;;
fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf update -y"
PKG_INSTALL="dnf install -y"
DISTRO_FAMILY="rhel"
RHEL_VERSION="9"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum update -y"
PKG_INSTALL="yum install -y"
DISTRO_FAMILY="rhel"
RHEL_VERSION="9"
;;
*)
error_exit "Unsupported distribution: $ID"
;;
esac
echo -e "${GREEN}Detected: $PRETTY_NAME${NC}"
# Update system packages
echo -e "${BLUE}[3/9] Updating system packages...${NC}"
$PKG_UPDATE || error_exit "Failed to update system packages"
# Install required tools
echo -e "${BLUE}[4/9] Installing required tools...${NC}"
case "$DISTRO_FAMILY" in
debian)
$PKG_INSTALL curl gnupg lsb-release || error_exit "Failed to install required tools"
;;
rhel)
$PKG_INSTALL curl gnupg || error_exit "Failed to install required tools"
;;
esac
# Install Tailscale
echo -e "${BLUE}[5/9] Installing Tailscale...${NC}"
case "$DISTRO_FAMILY" in
debian)
# Add Tailscale repository
KEYRING_FILE="/usr/share/keyrings/tailscale-archive-keyring.gpg"
LIST_FILE="/etc/apt/sources.list.d/tailscale.list"
CLEANUP_FILES+=("$KEYRING_FILE" "$LIST_FILE")
curl -fsSL "https://pkgs.tailscale.com/stable/ubuntu/$(lsb_release -cs).noarmor.gpg" | tee "$KEYRING_FILE" >/dev/null
curl -fsSL "https://pkgs.tailscale.com/stable/ubuntu/$(lsb_release -cs).tailscale-keyring.list" | tee "$LIST_FILE" >/dev/null
chmod 644 "$KEYRING_FILE" "$LIST_FILE"
apt update || error_exit "Failed to update package list"
$PKG_INSTALL tailscale || error_exit "Failed to install Tailscale"
;;
rhel)
# Add Tailscale repository
REPO_FILE="/etc/yum.repos.d/tailscale.repo"
curl -fsSL "https://pkgs.tailscale.com/stable/rhel/${RHEL_VERSION}/tailscale.repo" -o "$REPO_FILE"
chmod 644 "$REPO_FILE"
$PKG_INSTALL tailscale || error_exit "Failed to install Tailscale"
;;
esac
# Enable and start Tailscale service
echo -e "${BLUE}[6/9] Starting Tailscale service...${NC}"
systemctl enable --now tailscaled || error_exit "Failed to start Tailscale daemon"
# Wait for service to be ready
sleep 3
systemctl is-active --quiet tailscaled || error_exit "Tailscale daemon is not running"
# Configure IP forwarding
echo -e "${BLUE}[7/9] Configuring IP forwarding...${NC}"
SYSCTL_CONF="/etc/sysctl.d/99-tailscale.conf"
cat > "$SYSCTL_CONF" << EOF
# Tailscale subnet routing configuration
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
EOF
chmod 644 "$SYSCTL_CONF"
sysctl -p "$SYSCTL_CONF" || error_exit "Failed to apply sysctl settings"
# Configure firewall for subnet routing
echo -e "${BLUE}[8/9] Configuring firewall...${NC}"
case "$DISTRO_FAMILY" in
debian)
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q "Status: active"; then
echo -e "${YELLOW}Configuring UFW for Tailscale...${NC}"
ufw allow in on tailscale0
ufw allow out on tailscale0
fi
;;
rhel)
if systemctl is-active --quiet firewalld; then
echo -e "${YELLOW}Configuring firewalld for Tailscale...${NC}"
firewall-cmd --permanent --add-interface=tailscale0 --zone=trusted
firewall-cmd --permanent --add-masquerade
firewall-cmd --reload
fi
# Configure SELinux if enabled
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then
echo -e "${YELLOW}Configuring SELinux for Tailscale...${NC}"
setsebool -P nis_enabled 1 2>/dev/null || true
fi
;;
esac
# Initial Tailscale authentication and configuration
echo -e "${BLUE}[9/9] Configuring Tailscale...${NC}"
TAILSCALE_ARGS="--accept-routes --accept-dns"
# Add subnet routing
TAILSCALE_ARGS="$TAILSCALE_ARGS --advertise-routes=$SUBNET"
echo -e "${YELLOW}Subnet routing enabled for: $SUBNET${NC}"
# Add exit node if requested
if [ "$ENABLE_EXIT_NODE" = true ]; then
TAILSCALE_ARGS="$TAILSCALE_ARGS --advertise-exit-node"
echo -e "${YELLOW}Exit node functionality enabled${NC}"
fi
# Start Tailscale with configuration
echo -e "${YELLOW}Starting Tailscale authentication...${NC}"
tailscale up $TAILSCALE_ARGS || error_exit "Failed to configure Tailscale"
# Verify installation
echo -e "${BLUE}Verifying installation...${NC}"
if ! tailscale status >/dev/null 2>&1; then
error_exit "Tailscale is not properly configured"
fi
TAILSCALE_IP=$(tailscale ip -4 2>/dev/null || echo "Not assigned yet")
echo -e "${GREEN}✓ Tailscale service is running${NC}"
echo -e "${GREEN}✓ Tailscale IP: $TAILSCALE_IP${NC}"
echo -e "${GREEN}✓ Subnet routing configured for: $SUBNET${NC}"
[ "$ENABLE_EXIT_NODE" = true ] && echo -e "${GREEN}✓ Exit node capability enabled${NC}"
echo ""
echo -e "${GREEN}Installation completed successfully!${NC}"
echo ""
echo -e "${YELLOW}Next steps:${NC}"
echo "1. Approve subnet routes in the Tailscale admin console:"
echo " https://login.tailscale.com/admin/machines"
[ "$ENABLE_EXIT_NODE" = true ] && echo "2. Approve exit node capability in the admin console"
echo "3. Configure ACL policies for zero-trust access control"
echo "4. Install Tailscale on other devices to join the mesh network"
echo ""
echo -e "${BLUE}Tailscale status:${NC}"
tailscale status
Review the script before running. Execute with: bash install.sh