tailscale

SKILL.md

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=1 is 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 ping and tailscale netcheck to diagnose connectivity issues before blaming ACLs.
  • Key rotation — API keys and auth keys should be rotated regularly. Use short-lived keys where possible.
Weekly Installs
1
GitHub Stars
15
First Seen
3 days ago
Installed on
amp1
cline1
augment1
opencode1
cursor1
kimi-cli1