Tailscale
Overview
Tailscale is a mesh VPN built on WireGuard that connects your devices into a private network called a tailnet. Unlike traditional VPNs, Tailscale uses a peer-to-peer architecture — devices communicate directly when possible (using DERP relay servers as fallback) without traffic flowing through a central gateway. Features include MagicDNS (automatic device.tail123.ts.net hostnames), ACL policies, exit nodes, subnet routing, SSH key management, Funnel (exposing local services publicly), and Tailnet Lock (cryptographic key verification).
Installation
Linux (one-line installer)
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
Debian / Ubuntu
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 tailscale
sudo tailscale up
macOS
brew install tailscale
# Or download from https://tailscale.com/download/mac
sudo tailscaled &
tailscale up
Windows
winget install tailscale.tailscale
Docker (subnet router / exit node)
docker run -d \
--name tailscale \
--network host \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
-v /var/lib/tailscale:/var/lib/tailscale \
-e TS_AUTHKEY=tskey-auth-xxx \
-e TS_HOSTNAME=my-container \
tailscale/tailscale:latest
Verify
tailscale version
tailscale status
Configuration
Initial login and options
# Basic login (opens browser)
sudo tailscale up
# Login with auth key (headless/server)
sudo tailscale up --authkey tskey-auth-xxxxxxxxxx
# Login with reusable auth key
sudo tailscale up --authkey tskey-auth-xxx?ephemeral=false
# Login with all features enabled
sudo tailscale up \
--accept-routes \
--accept-dns \
--advertise-exit-node \
--ssh \
--hostname my-server
Admin Console (web UI)
Access at https://login.tailscale.com/admin/ to manage:
- Devices (approve, disable, remove)
- ACL policies (JSON-based firewall rules)
- DNS settings (MagicDNS, nameservers)
- Auth keys (ephemeral, reusable, pre-authorized)
- Users and roles
- Tailnet settings
ACL policy (admin console)
{
"acls": [
{
"action": "accept",
"src": ["tag:developer"],
"dst": ["tag:server:22,80,443"]
},
{
"action": "accept",
"src": ["autogroup:admin"],
"dst": ["*:*"]
}
],
"tagOwners": {
"tag:server": ["autogroup:admin"],
"tag:developer": ["autogroup:member"]
},
"grants": [
{
"src": ["tag:developer"],
"dst": ["tag:server"],
"app": {
"tailscale.com/cap/ssh": [{"action": "check"}]
}
}
]
}
Core Commands
| Command | Description |
|---|---|
tailscale up | Connect to tailnet |
tailscale down | Disconnect (keeps daemon running) |
tailscale logout | Log out and remove node from tailnet |
tailscale status | Show connected devices and IPs |
tailscale status --json | JSON status output |
tailscale ping hostname | Ping a tailnet device |
tailscale ip | Show your tailnet IP address |
tailscale ip -4 | Show IPv4 tailnet address only |
tailscale netcheck | Check NAT traversal and relay latency |
tailscale ssh user@device | SSH to a tailnet device |
tailscale serve 8080 | Serve local port 8080 on tailnet |
tailscale serve https / proxy http://localhost:3000 | HTTPS proxy on tailnet |
tailscale funnel 8080 | Expose port publicly via Funnel |
tailscale funnel off | Disable Funnel |
tailscale whois 100.x.y.z | Look up a tailnet IP |
tailscale dns status | Show DNS configuration |
tailscale lock init | Initialize Tailnet Lock |
tailscale lock add key | Trust a signing key |
tailscale set --ssh | Enable Tailscale SSH on device |
tailscale set --advertise-exit-node | Make device an exit node |
tailscale set --advertise-routes 10.0.0.0/24 | Advertise subnet routes |
Advanced Usage
Exit nodes
# On the device that will be the exit node:
sudo tailscale up --advertise-exit-node
# In admin console: approve the exit node under Machines tab
# On client: use the exit node
tailscale set --exit-node=server-hostname
tailscale set --exit-node-allow-lan-access=true # Keep LAN access
# Remove exit node
tailscale set --exit-node=
# Verify exit node is active
curl https://ifconfig.me # Should show exit node's IP
Subnet routing
# On the subnet router (connected to internal network):
sudo tailscale up --advertise-routes=10.0.0.0/24,192.168.1.0/24
# In admin console: approve the subnet routes
# On clients: enable accepting advertised routes
sudo tailscale up --accept-routes
# Verify route is accessible
tailscale status | grep subnets
ping 10.0.0.1 # Internal IP via subnet router
Tailscale SSH
# Enable Tailscale SSH on the server (replaces key management)
sudo tailscale up --ssh
# Connect from any device (no keys needed, uses Tailscale identity)
tailscale ssh ubuntu@my-server
# Or regular SSH via MagicDNS
ssh ubuntu@my-server.tail123.ts.net
Funnel (public HTTPS access)
# Expose a local web server publicly
tailscale funnel 3000
# Expose with a specific path
tailscale serve /api proxy http://localhost:8080/api
tailscale funnel 443
# Check current serve/funnel config
tailscale serve status
# Reset all serve config
tailscale serve reset
# Funnel creates a URL like: https://my-device.tail123.ts.net
Auth keys for automation
# Generate auth key in Admin Console > Settings > Keys
# Types:
# - One-time use (expires after first auth)
# - Reusable (can auth multiple devices)
# - Ephemeral (device removed when offline)
# - Pre-authorized (no approval needed)
# Use in CI/Docker/cloud-init
sudo tailscale up --authkey tskey-auth-xxxxxxxxxx \
--hostname ci-runner-01 \
--accept-routes
# Ephemeral key (auto-cleanup when agent stops)
sudo tailscale up --authkey tskey-auth-xxx?ephemeral=true
Device tags and ACL enforcement
# Apply tags at enrollment
sudo tailscale up --authkey tskey-auth-xxx \
--advertise-tags tag:server,tag:production
# Verify tags
tailscale status --json | jq '.Self.Tags'
MagicDNS and nameservers
# Enable MagicDNS in admin console under DNS tab
# Devices get hostnames: hostname.tail1234.ts.net
# Add split DNS (route company.internal to internal nameserver)
# In admin console DNS tab: add nameserver for specific domain
# Check DNS resolution
tailscale dns status
dig myserver.tail1234.ts.net
nslookup myserver
Tailnet Lock (advanced security)
# Initialize Tailnet Lock (cryptographic device verification)
tailscale lock init
# Generate a node key to sign
tailscale lock tskey
# Add a trusted key
tailscale lock add nlpub:xxxxxx
# Sign a node
tailscale lock sign <nodekey>
# View trusted keys
tailscale lock status
Tailscale feature comparison
| Feature | Free | Personal Pro | Enterprise |
|---|---|---|---|
| Devices | 100 | 100 | Unlimited |
| Users | 3 | 1 | Unlimited |
| Subnet routers | Yes | Yes | Yes |
| Exit nodes | Yes | Yes | Yes |
| Tailscale SSH | Yes | Yes | Yes |
| Funnel | Yes | Yes | Yes |
| Custom domains | No | No | Yes |
| SAML/SSO | No | No | Yes |
| Priority support | No | No | Yes |
Common Workflows
Remote development server
# Server setup
sudo tailscale up --ssh --hostname dev-server
# VS Code Remote SSH via Tailscale
# In ~/.ssh/config:
# Host dev-server
# HostName dev-server.tail1234.ts.net
# User ubuntu
code --remote ssh-remote+dev-server /home/ubuntu/project
Kubernetes cluster access via subnet router
# On a node inside the cluster network
sudo tailscale up \
--advertise-routes=10.96.0.0/12,10.244.0.0/16 \
--accept-routes
# Approve routes in admin console
# On developer machine
sudo tailscale up --accept-routes
kubectl --server=https://10.96.0.1 get pods
GitHub Actions with Tailscale
name: Deploy
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: tailscale/github-action@v2
with:
oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }}
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:ci
- name: Deploy to internal server
run: ssh ubuntu@my-internal-server.tail1234.ts.net 'make deploy'
Docker Compose internal network
# docker-compose.yml
services:
tailscale:
image: tailscale/tailscale:latest
network_mode: host
cap_add: [NET_ADMIN, NET_RAW]
environment:
TS_AUTHKEY: ${TS_AUTHKEY}
TS_HOSTNAME: my-compose-app
TS_EXTRA_ARGS: --advertise-tags=tag:compose
volumes:
- tailscale-data:/var/lib/tailscale
app:
image: myapp:latest
network_mode: "service:tailscale" # Share tailscale network namespace
volumes:
tailscale-data:
Tips and Best Practices
Use ephemeral keys for CI and short-lived containers. Ephemeral nodes are automatically removed from your tailnet when the device goes offline, keeping your device list clean without manual cleanup.
Enable MagicDNS and use hostnames everywhere. Tailscale IPs can change when re-enrolling a device. Hostnames via MagicDNS are stable and far more readable in config files and scripts.
Apply tags via auth keys, not post-enrollment. Tags set through --advertise-tags during tailscale up are locked to the auth key’s tag owner and cannot be self-modified, providing stronger access control guarantees.
Use subnet routers instead of installing Tailscale on every host. For large internal networks or cloud VPCs, deploy one subnet router per network segment rather than requiring Tailscale on every VM.
Restrict ACLs to minimum necessary access. The default policy allows all traffic between all devices. Replace it with explicit rules based on tags and roles. Use autogroup:admin only for admin devices.
Enable Tailscale SSH for passwordless, key-free SSH. Tailscale SSH replaces SSH key management with identity-based access tied to your SSO provider. Combined with ACL grants and device posture checks, it is more secure than traditional key files.
Use Funnel for temporary public access instead of opening firewall ports. Expose a local development server or webhook endpoint via Funnel rather than punching holes in your firewall or setting up an nginx reverse proxy.
Monitor device expiry. By default, devices require re-authentication every 180 days. Set key expiry policies in the admin console and configure pre-expiry notifications to avoid losing access to remote servers.
Use tailscale netcheck to diagnose connectivity issues. This command reports DERP relay latency, NAT type, and UDP availability — essential for debugging slow connections or failed peer-to-peer establishment.
Combine Tailscale SSH with device posture checks. Configure grants with "action": "check" rather than "accept" to require devices to meet security posture requirements (OS version, patch status) before SSH is allowed.