Last active
May 3, 2026 14:54
-
-
Save samelie/bddcbdc9038833615011a9fb75f159a4 to your computer and use it in GitHub Desktop.
Headscale SSH client setup - connect laptop to home machine via Tailscale
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # Headscale SSH Client Setup | |
| # Standalone script - run on any laptop to connect to home machine | |
| # | |
| # Usage: | |
| # curl -sL https://gist.githubusercontent.com/.../headscale-ssh-client-setup.sh | bash | |
| # # or | |
| # ./headscale-ssh-client-setup.sh | |
| # | |
| # Prerequisites: | |
| # - Tailscale app installed and connected to hs.add.dog | |
| # - Home machine (mac-mini) has Remote Login enabled | |
| set -euo pipefail | |
| ####################### | |
| # CONFIGURATION | |
| ####################### | |
| HOME_TAILSCALE_IP="100.64.0.3" | |
| HOME_HOSTNAME="mac-mini" | |
| HOME_MAGIC_DNS="mac-mini.tail.add.dog" | |
| SSH_USER="selie" | |
| HEADSCALE_URL="hs.add.dog" | |
| ####################### | |
| # COLORS | |
| ####################### | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| NC='\033[0m' | |
| ok() { echo -e "${GREEN}✓${NC} $1"; } | |
| warn() { echo -e "${YELLOW}⚠${NC} $1"; } | |
| fail() { echo -e "${RED}✗${NC} $1"; } | |
| ####################### | |
| # MAIN | |
| ####################### | |
| echo "╔════════════════════════════════════════════╗" | |
| echo "║ Headscale SSH Client Setup ║" | |
| echo "║ Target: $HOME_HOSTNAME @ $HOME_TAILSCALE_IP ║" | |
| echo "╚════════════════════════════════════════════╝" | |
| echo "" | |
| # 1. Check Tailscale | |
| echo "[1/6] Tailscale status" | |
| if ! command -v tailscale &>/dev/null; then | |
| fail "Tailscale not installed" | |
| echo " Install: https://tailscale.com/download" | |
| exit 1 | |
| fi | |
| if tailscale status &>/dev/null; then | |
| LOCAL_IP=$(tailscale ip -4 2>/dev/null || echo "unknown") | |
| ok "Connected as $LOCAL_IP" | |
| else | |
| fail "Tailscale not connected" | |
| echo " Run: tailscale up --login-server https://$HEADSCALE_URL" | |
| exit 1 | |
| fi | |
| # 2. SSH directory setup | |
| echo "" | |
| echo "[2/6] SSH directory" | |
| mkdir -p ~/.ssh/sockets | |
| chmod 700 ~/.ssh ~/.ssh/sockets 2>/dev/null || true | |
| ok "~/.ssh/sockets ready" | |
| # 3. SSH key | |
| echo "" | |
| echo "[3/6] SSH key" | |
| if [[ -f ~/.ssh/id_ed25519 ]]; then | |
| ok "Key exists: ~/.ssh/id_ed25519" | |
| else | |
| echo " Generating SSH key..." | |
| ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N "" -C "$(whoami)@$(hostname)" | |
| ok "Key generated" | |
| fi | |
| # 4. SSH config | |
| echo "" | |
| echo "[4/6] SSH config" | |
| SSH_CONFIG_BLOCK=" | |
| # === Headscale Remote SSH === | |
| Host home | |
| HostName $HOME_TAILSCALE_IP | |
| User $SSH_USER | |
| ControlMaster auto | |
| ControlPath ~/.ssh/sockets/%r@%h-%p | |
| ControlPersist 600 | |
| ServerAliveInterval 30 | |
| ServerAliveCountMax 3 | |
| Compression yes | |
| ForwardAgent yes | |
| UseKeychain yes | |
| AddKeysToAgent yes | |
| IdentityFile ~/.ssh/id_ed25519 | |
| # === End Headscale Remote SSH ===" | |
| if grep -q "# === Headscale Remote SSH ===" ~/.ssh/config 2>/dev/null; then | |
| ok "SSH config already configured" | |
| else | |
| touch ~/.ssh/config | |
| chmod 600 ~/.ssh/config | |
| echo "$SSH_CONFIG_BLOCK" >> ~/.ssh/config | |
| ok "Added 'home' host to ~/.ssh/config" | |
| fi | |
| # 5. Copy key to home machine | |
| echo "" | |
| echo "[5/6] SSH key authorization" | |
| # Test if key already authorized | |
| if ssh -o ConnectTimeout=5 -o BatchMode=yes -o StrictHostKeyChecking=accept-new home "exit 0" 2>/dev/null; then | |
| ok "Key already authorized on home machine" | |
| else | |
| warn "Key not yet authorized" | |
| echo "" | |
| echo " Copying SSH key to home machine..." | |
| echo " You'll be prompted for password (one time only):" | |
| echo "" | |
| if ssh-copy-id -i ~/.ssh/id_ed25519 "$SSH_USER@$HOME_TAILSCALE_IP"; then | |
| ok "Key copied successfully" | |
| else | |
| fail "Failed to copy key" | |
| echo " Manual fix: copy this to home's ~/.ssh/authorized_keys:" | |
| cat ~/.ssh/id_ed25519.pub | |
| exit 1 | |
| fi | |
| fi | |
| # 6. Verify connection | |
| echo "" | |
| echo "[6/6] Connection test" | |
| # Tailscale ping | |
| PING_OUT=$(tailscale ping -c 1 "$HOME_TAILSCALE_IP" 2>&1 | head -1) | |
| if echo "$PING_OUT" | grep -q "pong"; then | |
| if echo "$PING_OUT" | grep -q "DERP"; then | |
| warn "Relayed via DERP (works, slightly higher latency)" | |
| else | |
| ok "Direct WireGuard connection" | |
| fi | |
| else | |
| fail "Tailscale ping failed" | |
| fi | |
| # SSH test | |
| if ssh -o ConnectTimeout=10 home "echo 'SSH OK'" &>/dev/null; then | |
| ok "SSH connection successful" | |
| # Get remote info | |
| REMOTE_HOST=$(ssh home "hostname" 2>/dev/null || echo "unknown") | |
| REMOTE_OS=$(ssh home "sw_vers -productVersion 2>/dev/null || uname -r" 2>/dev/null || echo "unknown") | |
| echo " Remote: $REMOTE_HOST (macOS $REMOTE_OS)" | |
| else | |
| fail "SSH connection failed" | |
| echo " Debug: ssh -vvv home" | |
| exit 1 | |
| fi | |
| echo "" | |
| echo "╔════════════════════════════════════════════╗" | |
| echo "║ Setup Complete! ║" | |
| echo "╚════════════════════════════════════════════╝" | |
| echo "" | |
| echo "Usage:" | |
| echo " ssh home # Terminal" | |
| echo " code --remote ssh-remote+home /path # VS Code" | |
| echo " scp file.txt home:~/ # Copy files" | |
| echo "" | |
| echo "Troubleshooting:" | |
| echo " tailscale status # Check peers" | |
| echo " tailscale ping home # Test connection type" | |
| echo " ssh -vvv home # Verbose SSH debug" | |
| echo "" | |
| echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | |
| echo "" | |
| echo "Headscale Admin Reference (run on server or via kubectl):" | |
| echo "" | |
| echo " # List users (v0.28+ uses ID not name)" | |
| echo " headscale users list" | |
| echo "" | |
| echo " # Create pre-auth key (--user takes ID, not name)" | |
| echo " headscale preauthkeys create --user 1 --expiration 1h" | |
| echo "" | |
| echo " # Register new device (on device, use pre-auth key)" | |
| echo " tailscale up --login-server https://hs.add.dog --authkey <KEY>" | |
| echo "" | |
| echo " # List nodes" | |
| echo " headscale nodes list" | |
| echo "" | |
| echo " # Rename node" | |
| echo " headscale nodes rename --identifier <ID> <new-name>" | |
| echo "" | |
| echo " # Node expiry: 0001-01-01 or -62135596800 = never expires" | |
| echo " # ephemeral: false/null = permanent node" | |
| echo "" | |
| echo " # Via kubectl:" | |
| echo " kubectl exec -n headscale deploy/headscale -- headscale nodes list" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment