hardware/ubuntu-wsl/services/ssh.md
Table of Contents
sshd (WSL) โ direct SSH access, port 2204
Direct SSH access into the WSL Ubuntu distro, separate from RDP/console access to the Windows host itself.
Set up: 2026-09-20 ยท Host: Nuk ยท Distro: Ubuntu ยท ssh -p 2204 root@192.168.1.153
1. WSL side
openssh-serverinstalled. Ubuntu 26.04 ships it socket-activated on port 22 only (ssh.socket, hardcodedListenStream=0.0.0.0:22), so that socket unit was disabled andssh.serviceruns directly instead โ the only way to bind a non-22 port./etc/ssh/sshd_config:Port 2204 PermitRootLogin prohibit-password PasswordAuthentication no PubkeyAuthentication yes- User:
rootโ this distro has no separate non-root account. ~/.ssh/authorized_keys(i.e./root/.ssh/authorized_keys) holds the keys allowed in. Each client machine's key has to be added explicitly; a key working passwordlessly into Windows does not imply it's authorized into WSL โ separateauthorized_keys, separate trust store. Currently:- Windows box's own RSA key (
C:\Users\domin\.ssh\id_rsa.pub,dominick@dph.am) - MacBook Air ed25519 key (
dominick@MacBook-Air) โ added after first connection attempt from the Mac failed withPermission denied (publickey)because its key wasn't inauthorized_keysyet. - Mac Studio ed25519 key (
AAAA...VCIC, no comment set on the key itself) โ added 2026-09-20.
- Windows box's own RSA key (
- Autostart:
systemctl enable --now ssh.serviceโ starts with systemd whenever the distro boots. Same caveat as everything else in this folder: the distro itself has to be booted by something on the Windows side first (see../overview.md); systemd/sshd don't start until then.
systemctl status ssh.service
journalctl -u ssh -f
sshd -t # validate config after editing
systemctl restart ssh.service # after editing sshd_config
2. Windows side โ networking
This section is networking-mode-dependent โ check C:\Users\domin\.wslconfig for
networkingMode before touching anything here. Getting this wrong (leaving a NAT-mode
portproxy rule around after switching to mirrored) causes a very confusing symptom:
sshd fails to bind its own port with Address already in use and nothing on the WSL side
(ss, /proc/net/tcp, lsns) shows any process holding it โ because the real holder is
a Windows-side listener, invisible from inside the distro. Hit this once, 2026-09-21; took
a long diagnostic detour before realizing the mode had changed. Symptom checklist if this
recurs: systemctl status ssh.service shows failed/Address already in use, but ss -tlnp | grep 2204 inside WSL is empty โ that combination is this bug.
Mirrored mode (current, as of 2026-09-21)
.wslconfig has networkingMode=mirrored (+ hostAddressLoopback=true). WSL shares the
host's real network interfaces directly โ ip addr inside WSL shows eth1 carrying the
actual LAN IP 192.168.1.153, same as the Windows host. No portproxy is needed or
wanted: sshd binding 0.0.0.0:2204 inside WSL is already reachable at
192.168.1.153:2204 from the LAN, because WSL is on that interface now. Only the
firewall rule is needed:
New-NetFirewallRule -DisplayName "WSL SSH 2204" `
-Direction Inbound -Action Allow -Protocol TCP -LocalPort 2204
If a NAT-era portproxy rule for this port still exists, delete it โ in mirrored mode it keeps a listening socket open on the Windows side that collides with WSL's own bind attempt on the same (now-shared) interface stack, since the two are no longer isolated by NAT:
netsh interface portproxy delete v4tov4 listenport=2204 listenaddress=0.0.0.0
NAT mode (original setup, no longer active)
Same pattern as pi-web.md, pointed at loopback, not the WSL IP (which
changes every reboot in NAT mode โ loopback is stable because WSL's own
localhost-forwarding relays it regardless of the current WSL IP):
New-NetFirewallRule -DisplayName "WSL SSH 2204" `
-Direction Inbound -Action Allow -Protocol TCP -LocalPort 2204
netsh interface portproxy add v4tov4 `
listenaddress=0.0.0.0 listenport=2204 connectaddress=127.0.0.1 connectport=2204
An even earlier version pointed the portproxy at the dynamic WSL IP directly, refreshed by a Scheduled Task on every boot โ abandoned in favor of loopback (simpler, no task needed) the same day it was set up.
Check/manage (either mode)
netsh interface portproxy show v4tov4
Get-NetFirewallRule -DisplayName "WSL SSH 2204"
# inside WSL
ip addr show # confirms current networking mode by interface shape
ss -tlnp | grep 2204
3. Reachability
| From | Address |
|---|---|
| WSL itself | ssh -p 2204 root@127.0.0.1 (or localhost) |
| Windows | ssh -p 2204 root@127.0.0.1 (via WSL localhost forwarding) |
| LAN | ssh -p 2204 root@192.168.1.153 |
No public/WAN exposure was set up for this (unlike pi-wsl.dph.am's nginx ingress for
pi-web) โ LAN only, via whatever router-level access Dom already has to 192.168.1.153.
4. Troubleshooting
| Symptom | Cause / fix |
|---|---|
Permission denied (publickey) |
Client's public key isn't in /root/.ssh/authorized_keys inside WSL. Add it โ a key that's passwordless into Windows is not automatically trusted into WSL. |
.zshrc:bindkey:NN: cannot bind to an empty key sequence on login, and/or Backspace inserts a blank/space instead of deleting |
Client's $TERM doesn't have a matching entry in this box's terminfo database, so zsh can't look up key capabilities (khome, kend, kich1, kcbt, cub1, etc.) โ they resolve to empty strings, breaking both bindkey calls in ~/.zshrc and ZLE's screen redraw (which needs cub1/cursor-left to properly erase a char on backspace). Hit this 2026-09-21 from a Mac running Ghostty, which sends TERM=xterm-ghostty by default โ not present in a stock Ubuntu terminfo db even with ncurses-term installed. Fix from the Mac (in Ghostty, not inside the WSL session): infocmp -x xterm-ghostty | ssh -p 2204 root@192.168.1.153 -- tic -x - (a tic warning about "older tic versions may treat the description field as an alias" is harmless โ verify with infocmp -1 xterm-ghostty on the WSL side afterward). Open a new SSH session after running it; existing ones won't pick up the change. Alternative if you don't want to touch the remote terminfo db: set term = xterm-256color in Ghostty's config instead. ~/.zshrc was also hardened separately to guard those four bindkey calls with [[ -n "$key_info[X]" ]] && so an unrecognized $TERM degrades silently instead of spraying errors at login. |
| Connects fine locally, not from LAN | Check netsh interface portproxy show v4tov4 for the 2204 row and Get-NetFirewallRule -DisplayName "WSL SSH 2204"; re-create if missing (commands above, need elevated PowerShell). |
sshd -t fails after an edit |
Fix /etc/ssh/sshd_config before restarting โ a bad config leaves the old process running until the next restart attempt fails too. |
| Works right after setup, dead after a Windows reboot | Distro didn't boot up before you tried, or (if this ever regresses to IP-based portproxy instead of loopback) the WSL IP changed. Loopback portproxy avoids the second case entirely. |
5. Security notes
- Root login, key-only โ no password auth. Reasonable for a LAN-only personal dev box; would need hardening (non-root user, fail2ban equivalent, etc.) before any WAN exposure.
- Every additional client key added to
authorized_keysis a full root login into this box โ treat it like handing out a root password.