Sets up SSH access from another machine on the local network to a WSL (Ubuntu)
instance running on a Windows host. Runs on Windows via a single PowerShell
command — it drives the WSL side too through wsl -e bash -c.
┌──────────────┐ ┌──────────────────────┐ ┌────────────┐
│ other machine│──SSH▶│ Windows host (:22) │──────▶│ WSL (:22) │
│ (client) │ │ firewall (+portproxy)│ │ sshd │
└──────────────┘ └──────────────────────┘ └────────────┘
Two networking modes are supported:
- Classic (default) —
netsh portproxyforwards Windows:22 → WSL:22. Works everywhere, but the WSL IP changes on every Windows reboot, so the portproxy needs to be recreated (just re-runsetup.ps1). - Mirrored (
-Mirrored) — ⭐ recommended. WSL shares the Windows IP stack (networkingMode=mirroredin.wslconfig). No portproxy needed, immune to WSL IP changes. Requires Windows 11 + a recent WSL 2.
- Windows 10/11 with WSL 2 and at least one Linux distro installed (
wsl --install -d Ubuntu). - Windows 11 + recent WSL 2 for
-Mirroredmode. - A WSL terminal must be open (or the distro otherwise running) when you try to SSH in. WSL 2 shuts the distro's VM down after an idle timeout, and with it
sshdstops. As long as at least one WSL session is active (any terminal window, VS Code remote, etc.),sshdkeeps running and the SSH connection works. See Keep WSL running in the background below for ways to make this automatic.
Placeholders used throughout this README:
| Placeholder | What it means | Example |
|---|---|---|
<GH_USER>/<GH_REPO> |
Your GitHub user/repo hosting the scripts |
Bedatty-Engineering/wsl-ssh-setup |
<WSL_USER> |
Your username inside WSL | ubuntu |
<WINDOWS_LAN_IP> |
Windows host IP on the LAN (from ipconfig) |
192.168.x.y |
Run in a regular Windows PowerShell window — NOT as Administrator. The script self-elevates only the Windows-side commands (portproxy + firewall) via a UAC prompt. This guarantees WSL is invoked under your real user session, targeting the correct distro/user. Running elevated with a different admin account (a common case when UAC asks for credentials) would point wsl.exe at the wrong user's WSL instance.
Classic mode:
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
$p = "$env:TEMP\setup.ps1"
irm https://raw.githubusercontent.com/Bedatty-Engineering/wsl-ssh-setup/main/setup.ps1 -OutFile $p
& $pMirrored mode (⭐ recommended — immune to WSL IP changes):
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
$p = "$env:TEMP\setup.ps1"
irm https://raw.githubusercontent.com/Bedatty-Engineering/wsl-ssh-setup/main/setup.ps1 -OutFile $p
& $p -Mirrored
Set-ExecutionPolicy -Scope Processloosens the policy only for the current PowerShell window (nothing persists). This works regardless of your machine/user policy and avoids needingUnblock-File. Close the window and the policy reverts automatically.
Options:
& "$env:TEMP\setup.ps1" -ListenPort 2222 # non-default Windows port
& "$env:TEMP\setup.ps1" -Mirrored -ConnectPort 2222
& "$env:TEMP\setup.ps1" -WslDistro Ubuntu -WslUser alice # target a specific WSL distro/user
& "$env:TEMP\setup.ps1" -Yes # skip the confirmation promptBefore installing, the script shows which WSL distro and user it will target and asks for confirmation. Pass both -WslDistro and -WslUser (or -Yes) to make it fully non-interactive.
sudoinside WSL will prompt for your WSL password interactively. Keep the terminal focused.
⚠️ Piping remote scripts into a shell runs code blindly. For stronger guarantees, pin a specific commit (/main/→/<commit-sha>/) or download and inspect before executing.
Removes sshd from WSL, the portproxy, the firewall rule, and networkingMode=mirrored from .wslconfig (if present — skipped silently otherwise).
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force
$p = "$env:TEMP\teardown.ps1"
irm https://raw.githubusercontent.com/Bedatty-Engineering/wsl-ssh-setup/main/teardown.ps1 -OutFile $p
& $p -DisableMirroredssh <WSL_USER>@<WINDOWS_LAN_IP>Find the Windows IP with ipconfig (IPv4 of the LAN adapter).
ssh-keygen -t ed25519 # if you don't have a key yet
ssh-copy-id <WSL_USER>@<WINDOWS_LAN_IP>Shortcut in ~/.ssh/config on the client:
Host wsl-remote
HostName <WINDOWS_LAN_IP>
User <WSL_USER>
Connect with: ssh wsl-remote.
WSL 2 shuts down the distro VM after a short idle period when no processes are running. When the VM is down, sshd isn't listening and new SSH connections fail with Connection refused or timeout. You need at least one active session keeping the distro alive. Options, from simplest to most automatic:
Option 1 — just keep a WSL terminal window open. Works out of the box. The moment you close every terminal and wait a couple of minutes, the VM stops.
Option 2 — disable the idle shutdown entirely. Add to C:\Users\<you>\.wslconfig:
[wsl2]
vmIdleTimeout=-1Then wsl --shutdown and reopen WSL once. The VM will stay alive until you reboot Windows.
Option 3 — auto-start WSL at Windows logon. Create a shortcut to wsl.exe -d <YourDistro> -e bash -c "sudo service ssh start && sleep infinity" in shell:startup (run explorer shell:startup in the Run dialog). This launches a hidden WSL session on logon and keeps it running indefinitely, with sshd started.
Option 2 is the lowest-friction for a machine you SSH into regularly.
Orchestrators (run from Windows, drive both sides):
setup.ps1— installer. Runs the WSL setup viawsl -e, plus portproxy/firewall on Windows.teardown.ps1— uninstaller. Mirror image ofsetup.ps1.
Building blocks under lib/ (run directly if you prefer):
lib/setup-wsl.sh— inside WSL. Installs openssh-server, configures sshd on0.0.0.0:22.lib/setup-windows.ps1— Windows side only: portproxy + firewall.lib/teardown-wsl.sh— inside WSL. Purges openssh-server.lib/teardown-windows.ps1— Windows side only: removes portproxy and firewall.
If you'd rather not use the orchestrator:
Inside WSL:
curl -fsSL https://raw.githubusercontent.com/Bedatty-Engineering/wsl-ssh-setup/main/lib/setup-wsl.sh | bashOn Windows (PowerShell as Administrator):
$u = "https://raw.githubusercontent.com/Bedatty-Engineering/wsl-ssh-setup/main/lib/setup-windows.ps1"
irm $u -OutFile "$env:TEMP\setup-windows.ps1"; & "$env:TEMP\setup-windows.ps1"If the install finished with no errors but you still can't reach the WSL from another machine, work through these checks in order — each one isolates a different layer.
1. Confirm the target IP. You need the Windows LAN IP, not the WSL internal one.
ipconfig | Select-String IPv4Use the address that matches your network (e.g. 192.168.x.y). 172.* addresses are WSL-internal and not routable from other machines.
2. Confirm the client is on the same network.
# from the client
ping <WINDOWS_LAN_IP>If ping fails: both machines aren't on the same LAN, or the router has client/AP isolation enabled (common on guest networks).
3. Confirm the port is open from the client.
# Linux/macOS
nc -zv <WINDOWS_LAN_IP> 22
# Windows (from the client)
Test-NetConnection -ComputerName <WINDOWS_LAN_IP> -Port 22- timeout → Windows firewall is blocking. Check network profile (see below) and re-run
setup.ps1. - refused → nothing listening. Either portproxy is missing (classic mode) or sshd died.
4. Check the Windows network profile.
Firewall rules with Profile Any still need a reachable profile. Public is frequently restrictive.
Get-NetConnectionProfile
# If Public, switch to Private:
Set-NetConnectionProfile -InterfaceIndex <N> -NetworkCategory Private5. Test locally from Windows.
ssh <WSL_USER>@localhost -p 22- If this works from Windows but not from the LAN: the problem is on the firewall/routing side.
- If this fails too: sshd in WSL isn't reachable; continue.
6. Verify sshd inside WSL is actually running.
sudo service ssh status
sudo ss -tlnp | grep :22Must show LISTEN 0 128 0.0.0.0:22 sshd. If it's 127.0.0.1:22, sshd only accepts local connections — fix /etc/ssh/sshd_config to ListenAddress 0.0.0.0 and sudo service ssh restart.
7. (Classic mode) Check portproxy is pointing at the current WSL IP. The WSL IP changes on every Windows reboot.
netsh interface portproxy show all
wsl hostname -IThe Connect Address must match wsl hostname -I. If not, re-run setup.ps1.
8. (Mirrored mode) Check for port-22 conflict.
Get-Service sshd -ErrorAction SilentlyContinue
Get-NetTCPConnection -LocalPort 22 -ErrorAction SilentlyContinueIf Windows OpenSSH Server is running, it owns port 22 and WSL's sshd can't bind there. Stop it or use a different port (see the section below).
9. Check the Hyper-V firewall (Windows 11 only).
Get-NetFirewallHyperVProfile
Set-NetFirewallHyperVProfile -Name Public,Private,Domain -Enabled False10. Look at sshd logs in WSL.
sudo journalctl -u ssh -n 50 # if systemd is enabled
sudo tail -n 50 /var/log/auth.logFailed login attempts (wrong key, wrong user) show up here.
| Symptom | Likely cause | Action |
|---|---|---|
Connection timed out (plain) |
Windows firewall / wrong network | Re-run setup.ps1, check the client is on the same LAN |
Connection refused |
sshd not running or wrong port | In WSL: sudo service ssh status and sudo ss -tlnp | grep :22 |
Connection timed out during banner exchange |
Portproxy points to a stale WSL IP (classic mode) | Re-run setup.ps1, or switch to -Mirrored |
Connection reset |
Hyper-V Firewall, iptables in WSL, or TCP wrappers | See checks below |
Permission denied |
Wrong user/password or key not authorized | The user is the WSL user, not the Windows user |
Check from Windows whether WSL is reachable (classic mode):
ssh <WSL_USER>@(wsl hostname -I).Trim()Check Hyper-V Firewall (recent Windows 11):
Get-NetFirewallHyperVProfile
Set-NetFirewallHyperVProfile -Name Public,Private,Domain -Enabled FalseCheck network profile:
Get-NetConnectionProfile
# If Public, switch to Private:
Set-NetConnectionProfile -InterfaceIndex <N> -NetworkCategory PrivateShow current portproxy rules:
netsh interface portproxy show allsshd logs in WSL:
sudo journalctl -u ssh -n 50 # if systemd is enabled
sudo tail -f /var/log/auth.log # fallbackIn mirrored mode WSL binds to the Windows IP stack directly. If the Windows OpenSSH Server is running on port 22, sshd in WSL cannot use the same port. Either stop the Windows sshd (Stop-Service sshd; Set-Service sshd -StartupType Disabled) or run WSL's sshd on another port:
& "$env:TEMP\setup.ps1" -Mirrored -ConnectPort 2222 -ListenPort 2222(requires editing /etc/ssh/sshd_config in WSL to Port 2222 — or just re-run lib/setup-wsl.sh after adjusting the port manually in the config)
MIT — see LICENSE.