tailscale
Tailscale
Build zero-trust mesh networks that connect servers, laptops, and cloud instances as if they're on the same LAN. Built on WireGuard, managed through a control plane.
Setup
# Debian/Ubuntu
sudo apt-get install -y apt-transport-https
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null
curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list
sudo apt-get update && sudo apt-get install -y tailscale
sudo tailscale up
# Fedora/RHEL
sudo dnf config-manager --add-repo https://pkgs.tailscale.com/stable/fedora/tailscale.repo
sudo dnf install -y tailscale
sudo systemctl enable --now tailscaled
sudo tailscale up
# macOS
brew install tailscale
# Docker
docker run -d --name tailscale \
--cap-add NET_ADMIN --cap-add NET_RAW \
-v tailscale-state:/var/lib/tailscale \
-e TS_AUTHKEY=tskey-auth-... \
tailscale/tailscale
# Authenticate with an auth key (headless servers)
sudo tailscale up --authkey=tskey-auth-...
# Check status
tailscale status
Auth Keys
Generate at https://login.tailscale.com/admin/settings/keys:
- Reusable — same key for multiple machines (fleet provisioning)
- Ephemeral — machine removed when it disconnects (CI runners, containers)
- Pre-approved — skips admin approval
# Ephemeral node (auto-removed on disconnect — good for CI/CD runners)
tailscale up --authkey=tskey-auth-... --hostname=ci-runner-$(date +%s)
Access Control (ACLs)
ACLs define who can reach what. Edit at https://login.tailscale.com/admin/acls or via API:
{
"acls": [
// Developers can reach dev and staging servers
{"action": "accept", "src": ["group:devs"], "dst": ["tag:dev:*", "tag:staging:*"]},
// Only ops team can reach production
{"action": "accept", "src": ["group:ops"], "dst": ["tag:prod:*"]},
// Everyone can use DNS and HTTPS on shared services
{"action": "accept", "src": ["*"], "dst": ["tag:shared:80,443,53"]},
// SSH only for ops
{"action": "accept", "src": ["group:ops"], "dst": ["*:22"]}
],
"groups": {
"group:devs": ["user@example.com", "dev2@example.com"],
"group:ops": ["ops@example.com"]
},
"tagOwners": {
"tag:dev": ["group:devs"],
"tag:staging": ["group:ops"],
"tag:prod": ["group:ops"],
"tag:shared": ["group:ops"]
}
}
Subnet Router
Expose an entire network (e.g., AWS VPC, office LAN) to your tailnet without installing Tailscale on every machine:
# On the gateway machine in the target network
sudo tailscale up --advertise-routes=10.0.0.0/16,172.16.0.0/12
# Enable IP forwarding (required)
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
Approve routes in the admin panel or via API. Now every tailnet member can reach 10.0.0.0/16 through the subnet router.
Exit Node
Route all internet traffic through a specific machine (like a traditional VPN):
# On the exit node (e.g., a VPS in a specific country)
sudo tailscale up --advertise-exit-node
# On the client — use the exit node
sudo tailscale up --exit-node=exit-server-hostname
# Verify — should show the exit node's IP
curl ifconfig.me
MagicDNS
Every machine gets a DNS name: hostname.tailnet-name.ts.net. No IP addresses to remember:
# Access your home server from anywhere
ssh home-server.tailnet.ts.net
# Connect to a database
psql -h db-server.tailnet.ts.net -U postgres
# Internal service discovery
curl http://api-server.tailnet.ts.net:3000/health
Tailscale SSH
Replace SSH key management entirely. Tailscale authenticates SSH connections using your identity:
# Enable Tailscale SSH on a server
sudo tailscale up --ssh
# Connect — no keys needed, authenticated by Tailscale identity
ssh user@server.tailnet.ts.net
# SSH ACL (add to your ACL policy)
# "ssh": [
# {"action": "accept", "src": ["group:ops"], "dst": ["tag:prod"], "users": ["root"]},
# {"action": "accept", "src": ["group:devs"], "dst": ["tag:dev"], "users": ["deploy"]}
# ]
Funnel (expose to internet)
Expose a local service to the public internet through Tailscale's infrastructure — no port forwarding, no dynamic DNS:
# Serve a local port publicly
tailscale funnel 3000
# Custom hostname (requires DNS setup)
tailscale funnel --set-path /api 8080
# HTTPS serve (local dev with real TLS cert)
tailscale serve https / http://localhost:3000
tailscale serve status # Show what's being served
API Automation
"""tailscale_api.py — Manage tailnet programmatically."""
import requests
API = "https://api.tailscale.com/api/v2"
TAILNET = "example.com" # Your tailnet name
API_KEY = "tskey-api-..." # Generate at admin panel → Settings → Keys
headers = {"Authorization": f"Bearer {API_KEY}"}
def list_devices() -> list:
"""List all devices in the tailnet with their IPs and status."""
resp = requests.get(f"{API}/tailnet/{TAILNET}/devices", headers=headers)
return resp.json()["devices"]
def set_device_tags(device_id: str, tags: list[str]):
"""Set ACL tags on a device (e.g., ['tag:prod', 'tag:db']).
Args:
device_id: Device ID from list_devices.
tags: List of ACL tags to apply.
"""
requests.post(f"{API}/device/{device_id}/tags",
json={"tags": tags}, headers=headers)
def create_auth_key(reusable: bool = False, ephemeral: bool = False,
tags: list[str] = None) -> str:
"""Create an auth key for automated device registration.
Args:
reusable: Allow key to register multiple devices.
ephemeral: Devices auto-removed on disconnect.
tags: ACL tags applied to devices using this key.
"""
body = {
"capabilities": {
"devices": {
"create": {
"reusable": reusable,
"ephemeral": ephemeral,
"tags": tags or [],
}
}
}
}
resp = requests.post(f"{API}/tailnet/{TAILNET}/keys",
json=body, headers=headers)
return resp.json()["key"]
def approve_routes(device_id: str, routes: list[str]):
"""Approve advertised subnet routes for a device.
Args:
device_id: Device ID.
routes: Routes to approve (e.g., ['10.0.0.0/16']).
"""
resp = requests.post(f"{API}/device/{device_id}/routes",
json={"routes": routes}, headers=headers)
return resp.json()
Fleet Provisioning
#!/bin/bash
# provision-fleet.sh — Add a batch of servers to the tailnet
SERVERS=("10.0.1.10" "10.0.1.11" "10.0.1.12" "10.0.1.13")
TAGS="tag:prod,tag:api"
# Create a reusable, pre-approved auth key with tags
AUTH_KEY=$(curl -s -X POST "https://api.tailscale.com/api/v2/tailnet/$TAILNET/keys" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{\"capabilities\":{\"devices\":{\"create\":{\"reusable\":true,\"tags\":[\"$TAGS\"]}}}}" \
| jq -r '.key')
for server in "${SERVERS[@]}"; do
ssh "$server" "sudo apt-get update && sudo apt-get install -y tailscale && \
sudo tailscale up --authkey=$AUTH_KEY --hostname=api-\$(hostname -s)" &
done
wait
echo "Fleet provisioning complete"
Docker Integration
Run services inside Docker accessible via tailnet:
# docker-compose.yml — Service accessible via Tailscale
services:
tailscale:
image: tailscale/tailscale
cap_add: [NET_ADMIN, NET_RAW]
volumes:
- tailscale-state:/var/lib/tailscale
environment:
TS_AUTHKEY: tskey-auth-...
TS_EXTRA_ARGS: --hostname=my-service
TS_SERVE_CONFIG: /config/serve.json
volumes:
- ./serve.json:/config/serve.json
app:
image: my-app:latest
network_mode: service:tailscale # Share Tailscale's network namespace
volumes:
tailscale-state:
Guidelines
- Auth keys for automation — never use interactive login for servers. Generate auth keys with appropriate tags.
- Tag everything — ACLs are tag-based. Untagged devices can't be referenced in policies. Tag at provisioning time.
- Ephemeral keys for ephemeral workloads — CI runners, preview environments, and containers should auto-remove when they disconnect.
- Subnet routers need IP forwarding — forgetting
sysctl net.ipv4.ip_forward=1is the #1 setup issue. - MagicDNS replaces IP addresses — use hostnames everywhere. IPs change; DNS names don't.
- Funnel for webhooks — expose a local development endpoint for testing webhooks from third-party services without ngrok.
- ACL testing — use
tailscale pingandtailscale netcheckto diagnose connectivity issues before blaming ACLs. - Key rotation — API keys and auth keys should be rotated regularly. Use short-lived keys where possible.