Remote SSH access to Windows, Debian, and Alpine via a single Tailscale node.
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.
- 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
All commands in sections marked (Admin) must be run in an elevated PowerShell terminal.
# Authenticate (opens browser)
tailscale login
# Configure hostname and unattended mode
tailscale up --hostname=zap --unattendedGo to https://login.tailscale.com/admin/acls and set:
{
"acls": [
{ "action": "accept", "src": ["*"], "dst": ["*:*"] }
]
}This allows all traffic between your devices.
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell `
-Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" `
-PropertyType String -ForceReplace 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
Set-Service sshd -StartupType Automatic
Restart-Service sshdNew-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/etc/wsl.conf:
[boot]
systemd=trueSSH access comes through Windows sshd on port 2223.
/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_PIDFrom 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.
- Windows boots → Tailscale service starts (auto) → node
zapcomes online - WSL distros start → Debian and Alpine boot up
- Alpine wsl-startup.sh runs → starts dockerd and other services
- Debian systemd boots → services start normally
- sshd is already listening on ports 2222/2223/2224 → all three targets reachable
No login required. Everything starts at boot.
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.