Skip to content

Instantly share code, notes, and snippets.

@bstee615
Last active September 9, 2026 21:45
Show Gist options
  • Select an option

  • Save bstee615/d510992d535e955fb175028eb5c5c4d0 to your computer and use it in GitHub Desktop.

Select an option

Save bstee615/d510992d535e955fb175028eb5c5c4d0 to your computer and use it in GitHub Desktop.
Tailscale + SSH setup for Windows + WSL2 (multi-port sshd)

Tailscale + SSH Setup for Windows + WSL2

Remote SSH access to Windows, Debian, and Alpine via a single Tailscale node.

Architecture

iPhone / Remote Device
  │
  │  Tailscale VPN
  ▼
┌─────────────────────────────────────────┐
│  Windows (Tailscale node: "zap")        │
│  100.x.x.x                             │
│                                         │
│  OpenSSH Server (sshd)                  │
│  ├── Port 2222 → PowerShell (Windows)   │
│  ├── Port 2223 → wsl -d Debian          │
│  └── Port 2224 → wsl -d Alpine          │
│                                         │
│  wsl startup keeps distros alive        │
└─────────────────────────────────────────┘

Why this design:

  • iOS Tailscale intercepts port 22, so we use 2222+.
  • WSL2 distros share a network namespace — you can't run independent sshd per distro on port 22.
  • One Tailscale node avoids TUN device conflicts between distros.

Prerequisites

  • Windows 11 with WSL2
  • Tailscale installed on Windows (winget install Tailscale.Tailscale)
  • OpenSSH Server installed (Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0)
  • Debian and Alpine WSL distros installed

Setup

All commands in sections marked (Admin) must be run in an elevated PowerShell terminal.

1. Tailscale (Windows)

# Authenticate (opens browser)
tailscale login

# Configure hostname and unattended mode
tailscale up --hostname=zap --unattended

2. Tailscale ACL Policy

Go to https://login.tailscale.com/admin/acls and set:

{
  "acls": [
    { "action": "accept", "src": ["*"], "dst": ["*:*"] }
  ]
}

This allows all traffic between your devices.

3. OpenSSH Server (Admin)

Set default shell to PowerShell

New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
  -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" `
  -PropertyType String -Force

Write sshd_config

Replace C:\ProgramData\ssh\sshd_config with:

# Listen on ports 2222 (Windows), 2223 (Debian), 2224 (Alpine)
Port 2222
Port 2223
Port 2224

AuthorizedKeysFile .ssh/authorized_keys

Subsystem sftp sftp-server.exe

# Port 2223: Debian WSL
Match LocalPort 2223
       ForceCommand C:\Windows\System32\wsl.exe -d Debian --cd ~

# Port 2224: Alpine WSL
Match LocalPort 2224
       ForceCommand C:\Windows\System32\wsl.exe -d Alpine --cd ~

# Admin users use separate authorized_keys
Match Group administrators
       AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys

Configure sshd service

Set-Service sshd -StartupType Automatic
Restart-Service sshd

Firewall rules

New-NetFirewallRule -DisplayName "SSH Windows (2222)" -Direction Inbound -LocalPort 2222 -Protocol TCP -Action Allow
New-NetFirewallRule -DisplayName "SSH Debian (2223)" -Direction Inbound -LocalPort 2223 -Protocol TCP -Action Allow
New-NetFirewallRule -DisplayName "SSH Alpine (2224)" -Direction Inbound -LocalPort 2224 -Protocol TCP -Action Allow

4. Debian WSL Config

/etc/wsl.conf:

[boot]
systemd=true

SSH access comes through Windows sshd on port 2223.

5. Alpine WSL Config

/etc/wsl.conf:

[boot]
command = /usr/local/bin/wsl-startup.sh

[user]
default=myuser

[network]
generateResolvConf = false

/usr/local/bin/wsl-startup.sh:

#!/bin/sh
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
echo "$(date): WSL Alpine startup" >> /var/log/wsl-boot.log

mkdir -p /run/openrc/started
echo 'default' > /run/openrc/softlevel

if ! pgrep -x sshd > /dev/null 2>&1; then
    /usr/sbin/sshd 2>/dev/null || true
    ln -sf /etc/init.d/sshd /run/openrc/started/sshd 2>/dev/null || true
fi

/usr/bin/dockerd >> /var/log/dockerd.log 2>&1 &
DOCKERD_PID=$!
echo "$(date): dockerd started (pid $DOCKERD_PID)" >> /var/log/wsl-boot.log

i=0
while [ $i -lt 90 ]; do
    [ -S /run/docker.sock ] && break
    sleep 1
    i=$((i + 1))
done
echo "$(date): docker socket ready after ${i}s" >> /var/log/wsl-boot.log

# Fallback DNS (generateResolvConf=false in wsl.conf)
printf "nameserver 1.1.1.1\nnameserver 8.8.8.8\n" > /etc/resolv.conf

wait $DOCKERD_PID

Usage

From any device on the tailnet:

Command Destination
ssh -p 2222 myuser@zap Windows PowerShell
ssh -p 2223 myuser@zap Debian bash
ssh -p 2224 myuser@zap Alpine sh

Save these as profiles in your SSH client (Shelly, Termius, etc.) for one-tap access.

Boot Sequence

  1. Windows boots → Tailscale service starts (auto) → node zap comes online
  2. WSL distros start → Debian and Alpine boot up
  3. Alpine wsl-startup.sh runs → starts dockerd and other services
  4. Debian systemd boots → services start normally
  5. sshd is already listening on ports 2222/2223/2224 → all three targets reachable

No login required. Everything starts at boot.

Troubleshooting

sshd won't start: Check config syntax: sshd -t (in admin terminal).

WSL distros not starting: Ensure WSL distros are configured to auto-start, or use a watchdog service.

Tailscale not connecting: Check tailscale status. Re-auth with tailscale login if needed.

Port 22 goes to wrong place: iOS Tailscale intercepts port 22. Always use 2222+ ports.

DNS issues in Alpine: Ensure generateResolvConf = false in /etc/wsl.conf and fallback DNS is set in startup script.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment