Set up Tailscale mesh VPN with Kubernetes cluster integration for secure pod-to-pod communication, subnet routing, and service discovery across distributed nodes.
Prerequisites
- Running Kubernetes cluster
- Tailscale account with admin access
- Root access on all nodes
- kubectl configured
What this solves
Tailscale provides secure, encrypted networking between Kubernetes nodes without complex VPN infrastructure. This tutorial shows you how to integrate Tailscale with your Kubernetes cluster to enable secure pod-to-pod communication, implement subnet routing for cluster networks, and configure service discovery across distributed environments.
Step-by-step configuration
Update system packages
Start by updating your package manager on all Kubernetes nodes to ensure compatibility with Tailscale.
sudo apt update && sudo apt upgrade -y
Install Tailscale on Kubernetes nodes
Add the Tailscale repository and install the client on each Kubernetes node. This enables secure networking between cluster nodes.
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list
sudo apt update
sudo apt install -y tailscale
Start Tailscale daemon
Enable and start the Tailscale daemon on all nodes. This creates the encrypted mesh network foundation.
sudo systemctl enable --now tailscaled
sudo systemctl status tailscaled
Authenticate control plane node
Connect the Kubernetes control plane node to your Tailscale network with subnet routing enabled. This allows the node to advertise cluster subnets.
sudo tailscale up --advertise-routes=10.96.0.0/12,10.244.0.0/16 --accept-routes
tailscale ip -4
kubectl cluster-info dump | grep -E 'service-cluster-ip-range|cluster-cidr'.Authenticate worker nodes
Connect each worker node to the Tailscale network with route acceptance enabled. This allows nodes to access cluster services through the control plane.
sudo tailscale up --accept-routes
tailscale status
Configure Tailscale ACL policies
Create Access Control List policies in the Tailscale admin console to secure cluster communication. Navigate to the Tailscale admin panel and update your ACL policy.
{
"groups": {
"group:k8s-nodes": ["user1@example.com", "user2@example.com"]
},
"tagOwners": {
"tag:k8s-control": ["group:k8s-nodes"],
"tag:k8s-worker": ["group:k8s-nodes"]
},
"acls": [
{
"action": "accept",
"src": ["tag:k8s-control", "tag:k8s-worker"],
"dst": ["tag:k8s-control:", "tag:k8s-worker:"]
},
{
"action": "accept",
"src": ["group:k8s-nodes"],
"dst": ["tag:k8s-control:6443", "tag:k8s-control:2379-2380"]
}
]
}
Apply node tags
Tag your Kubernetes nodes in the Tailscale admin console to apply the ACL policies. This enables role-based access control for cluster components.
# On control plane node
sudo tailscale up --advertise-routes=10.96.0.0/12,10.244.0.0/16 --accept-routes --advertise-tags=tag:k8s-control
On worker nodes
sudo tailscale up --accept-routes --advertise-tags=tag:k8s-worker
Enable subnet routing approval
Approve the subnet routes in the Tailscale admin console to allow cluster traffic routing. This step must be completed in the web interface.
Deploy Tailscale operator
Install the Tailscale Kubernetes operator to manage subnet routing and service exposure automatically. This provides native Kubernetes integration.
kubectl apply -f https://raw.githubusercontent.com/tailscale/tailscale/main/cmd/k8s-operator/deploy/manifests/operator.yaml
Create operator authentication secret
Generate an auth key in the Tailscale admin console and create a Kubernetes secret for the operator authentication.
kubectl create secret generic operator-oauth --from-literal=client_id=your-client-id --from-literal=client_secret=your-client-secret -n tailscale
Configure service discovery integration
Create a ConfigMap to configure automatic service discovery and DNS integration for Tailscale-connected services.
apiVersion: v1
kind: ConfigMap
metadata:
name: tailscale-config
namespace: tailscale
data:
TS_KUBE_SECRET: "operator-oauth"
TS_USERSPACE: "false"
TS_AUTH_ONCE: "true"
TS_ACCEPT_DNS: "true"
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: tailscale-subnet-router
namespace: tailscale
spec:
selector:
matchLabels:
app: tailscale-subnet-router
template:
metadata:
labels:
app: tailscale-subnet-router
spec:
serviceAccountName: tailscale-operator
hostNetwork: true
containers:
- name: tailscale
image: tailscale/tailscale:latest
env:
- name: TS_KUBE_SECRET
value: "operator-oauth"
- name: TS_ROUTES
value: "10.96.0.0/12,10.244.0.0/16"
- name: TS_ACCEPT_DNS
value: "true"
securityContext:
capabilities:
add:
- NET_ADMIN
- NET_RAW
volumeMounts:
- name: dev-net-tun
mountPath: /dev/net/tun
volumes:
- name: dev-net-tun
hostPath:
path: /dev/net/tun
kubectl apply -f tailscale-config.yaml
Configure pod-to-pod communication
Create a NetworkPolicy to allow secure communication between pods across Tailscale-connected nodes. This enables encrypted inter-pod communication.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: tailscale-pod-communication
namespace: default
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: tailscale
- podSelector:
matchLabels:
networking/tailscale: "enabled"
egress:
- to:
- namespaceSelector:
matchLabels:
name: tailscale
- podSelector:
matchLabels:
networking/tailscale: "enabled"
- to: []
ports:
- protocol: UDP
port: 53
kubectl apply -f tailscale-network-policy.yaml
Expose services via Tailscale
Configure a LoadBalancer service to expose cluster services through Tailscale networking. This allows external access to cluster services via the mesh network.
apiVersion: v1
kind: Service
metadata:
name: example-app-tailscale
annotations:
tailscale.com/expose: "true"
tailscale.com/hostname: "k8s-example-app"
spec:
type: LoadBalancer
loadBalancerClass: tailscale
selector:
app: example-app
ports:
- port: 80
targetPort: 8080
protocol: TCP
kubectl apply -f tailscale-service.yaml
Verify your setup
Test the Tailscale and Kubernetes integration to ensure secure networking is functioning correctly.
# Check Tailscale status on all nodes
tailscale status
tailscale ip -4
Verify subnet routes are advertised
tailscale status --json | grep -A5 "AdvertiseRoutes"
Check Kubernetes connectivity
kubectl get nodes -o wide
kubectl get services -A
Test pod-to-pod communication
kubectl run test-pod --image=busybox --restart=Never -- sleep 3600
kubectl exec test-pod -- nslookup kubernetes.default.svc.cluster.local
Verify Tailscale operator
kubectl get pods -n tailscale
kubectl logs -n tailscale deployment/tailscale-operator
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Subnet routes not working | Routes not approved in admin console | Enable subnet routes in Tailscale admin panel under machine settings |
| Pods can't reach services | Incorrect CIDR ranges in advertise-routes | Check actual cluster CIDRs with kubectl cluster-info dump | grep -E 'service-cluster-ip-range|cluster-cidr' |
| Tailscale operator failing | Missing authentication credentials | Recreate the operator-oauth secret with valid client ID and secret |
| Network policy blocking traffic | Restrictive NetworkPolicy rules | Add appropriate pod labels and update NetworkPolicy selectors |
| DNS resolution failing | DNS not configured in Tailscale | Enable MagicDNS in Tailscale admin console and use --accept-dns flag |
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'
NC='\033[0m'
# Configuration
CONTROL_PLANE_SUBNETS="${CONTROL_PLANE_SUBNETS:-10.96.0.0/12,10.244.0.0/16}"
NODE_TYPE="${NODE_TYPE:-}"
TAILSCALE_TAG="${TAILSCALE_TAG:-}"
usage() {
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " --node-type control|worker - Kubernetes node type (required)"
echo " --subnets CIDR1,CIDR2 - Subnet routes for control plane (default: 10.96.0.0/12,10.244.0.0/16)"
echo " --tag TAG - Tailscale tag (e.g., tag:k8s-control or tag:k8s-worker)"
echo " --help - Show this help message"
echo ""
echo "Examples:"
echo " $0 --node-type control --tag tag:k8s-control"
echo " $0 --node-type worker --tag tag:k8s-worker"
exit 1
}
log() {
echo -e "${GREEN}[INFO]${NC} $1"
}
warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
error() {
echo -e "${RED}[ERROR]${NC} $1"
}
cleanup() {
if [ $? -ne 0 ]; then
error "Installation failed. Rolling back..."
systemctl stop tailscaled 2>/dev/null || true
systemctl disable tailscaled 2>/dev/null || true
if [ "$PKG_MGR" = "apt" ]; then
apt remove -y tailscale 2>/dev/null || true
rm -f /usr/share/keyrings/tailscale-archive-keyring.gpg
rm -f /etc/apt/sources.list.d/tailscale.list
elif [ "$PKG_MGR" = "dnf" ] || [ "$PKG_MGR" = "yum" ]; then
$PKG_INSTALL remove -y tailscale 2>/dev/null || true
rm -f /etc/yum.repos.d/tailscale.repo
fi
fi
}
trap cleanup ERR
check_root() {
if [ "$EUID" -ne 0 ]; then
error "This script must be run as root or with sudo"
exit 1
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_UPGRADE="apt upgrade -y"
PKG_INSTALL="apt install -y"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_UPDATE="dnf check-update"
PKG_UPGRADE="dnf update -y"
PKG_INSTALL="dnf install -y"
;;
amzn)
PKG_MGR="yum"
PKG_UPDATE="yum check-update"
PKG_UPGRADE="yum update -y"
PKG_INSTALL="yum install -y"
;;
*)
error "Unsupported distribution: $ID"
exit 1
;;
esac
else
error "Cannot detect distribution. /etc/os-release not found."
exit 1
fi
}
parse_args() {
while [[ $# -gt 0 ]]; do
case $1 in
--node-type)
NODE_TYPE="$2"
shift 2
;;
--subnets)
CONTROL_PLANE_SUBNETS="$2"
shift 2
;;
--tag)
TAILSCALE_TAG="$2"
shift 2
;;
--help)
usage
;;
*)
error "Unknown option: $1"
usage
;;
esac
done
if [ -z "$NODE_TYPE" ]; then
error "Node type is required (--node-type control|worker)"
usage
fi
if [ "$NODE_TYPE" != "control" ] && [ "$NODE_TYPE" != "worker" ]; then
error "Invalid node type. Must be 'control' or 'worker'"
usage
fi
}
update_system() {
echo "[1/7] Updating system packages..."
$PKG_UPDATE || true
$PKG_UPGRADE
log "System packages updated successfully"
}
install_tailscale() {
echo "[2/7] Installing Tailscale..."
if [ "$PKG_MGR" = "apt" ]; then
# Install prerequisites
$PKG_INSTALL curl gnupg lsb-release
# Add Tailscale repository
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list
chmod 644 /usr/share/keyrings/tailscale-archive-keyring.gpg
chmod 644 /etc/apt/sources.list.d/tailscale.list
# Update and install
$PKG_UPDATE
$PKG_INSTALL tailscale
elif [ "$PKG_MGR" = "dnf" ] || [ "$PKG_MGR" = "yum" ]; then
# Install prerequisites
$PKG_INSTALL curl
# Add Tailscale repository
if [ "$PKG_MGR" = "dnf" ]; then
dnf config-manager --add-repo https://pkgs.tailscale.com/stable/rhel/9/tailscale.repo
else
curl -fsSL https://pkgs.tailscale.com/stable/rhel/7/tailscale.repo | tee /etc/yum.repos.d/tailscale.repo
fi
# Install
$PKG_INSTALL tailscale
fi
log "Tailscale installed successfully"
}
start_tailscale() {
echo "[3/7] Starting Tailscale daemon..."
systemctl enable tailscaled
systemctl start tailscaled
# Wait for daemon to be ready
sleep 3
# Verify daemon is running
if systemctl is-active --quiet tailscaled; then
log "Tailscale daemon started successfully"
else
error "Failed to start Tailscale daemon"
exit 1
fi
}
authenticate_node() {
echo "[4/7] Configuring Tailscale authentication..."
local tailscale_cmd="tailscale up --accept-routes"
if [ "$NODE_TYPE" = "control" ]; then
tailscale_cmd="tailscale up --advertise-routes=${CONTROL_PLANE_SUBNETS} --accept-routes"
log "Configuring control plane node with subnet routes: $CONTROL_PLANE_SUBNETS"
else
log "Configuring worker node"
fi
if [ -n "$TAILSCALE_TAG" ]; then
tailscale_cmd="$tailscale_cmd --advertise-tags=$TAILSCALE_TAG"
log "Using Tailscale tag: $TAILSCALE_TAG"
fi
warn "Please authenticate this node by visiting the URL that will be displayed:"
$tailscale_cmd
log "Node authentication completed"
}
verify_connection() {
echo "[5/7] Verifying Tailscale connection..."
# Wait for connection to establish
sleep 5
# Check if we have an IP
local tailscale_ip
tailscale_ip=$(tailscale ip -4 2>/dev/null || echo "")
if [ -n "$tailscale_ip" ]; then
log "Tailscale IP assigned: $tailscale_ip"
else
warn "No Tailscale IP assigned yet. Connection may still be establishing."
fi
# Show status
tailscale status
log "Connection verification completed"
}
configure_firewall() {
echo "[6/7] Configuring firewall rules..."
# Configure firewall based on available tools
if command -v ufw >/dev/null 2>&1; then
# Ubuntu/Debian UFW
ufw allow in on tailscale0
log "UFW firewall rules configured"
elif command -v firewall-cmd >/dev/null 2>&1; then
# RHEL/CentOS firewalld
if systemctl is-active --quiet firewalld; then
firewall-cmd --permanent --zone=trusted --add-interface=tailscale0
firewall-cmd --reload
log "Firewalld rules configured"
fi
elif command -v iptables >/dev/null 2>&1; then
# Fallback to iptables
iptables -I INPUT -i tailscale0 -j ACCEPT
log "Iptables rules configured"
else
warn "No supported firewall detected. Manual configuration may be required."
fi
}
display_next_steps() {
echo "[7/7] Installation complete!"
log "Tailscale has been successfully installed and configured"
echo ""
echo "Next steps:"
echo "1. If this is a control plane node, approve subnet routes in the Tailscale admin console:"
echo " - Navigate to https://login.tailscale.com/admin/machines"
echo " - Find this node and click the three dots menu"
echo " - Select 'Edit route settings' and enable the advertised routes"
echo ""
echo "2. Configure ACL policies in the Tailscale admin console for secure access"
echo ""
echo "3. Verify connectivity between nodes:"
echo " tailscale ping <other-node-ip>"
echo ""
echo "4. Check cluster network access from remote locations"
echo ""
if [ "$NODE_TYPE" = "control" ]; then
warn "Remember to approve subnet routes (${CONTROL_PLANE_SUBNETS}) in the Tailscale admin console"
fi
}
main() {
check_root
detect_distro
parse_args "$@"
log "Starting Tailscale installation for Kubernetes $NODE_TYPE node on $ID"
update_system
install_tailscale
start_tailscale
authenticate_node
verify_connection
configure_firewall
display_next_steps
log "Tailscale installation completed successfully!"
}
main "$@"
Review the script before running. Execute with: bash install.sh