Setting Up Passwordless SSH from macOS to Windows over Tailscale: Lessons Learned
Published on June 27, 2026
I recently configured a macOS machine to remotely develop on a Windows laptop (primarily using WSL) over Tailscale. While the final experience is fantastic, there were quite a few platform-specific quirks along the way.
These are the lessons I learned.
1. Tailscale is only the network
One misconception I had initially was that Tailscale would also handle SSH authentication.
It doesn’t.
Tailscale provides:
- Secure encrypted connectivity
- Private IP addressing
- NAT traversal
- Device authentication
OpenSSH still handles:
- Password authentication
- Public key authentication
- Authorization
Think of it as:
Mac
│
│ SSH
▼
Tailscale Network
▼
Windows OpenSSH Server
The VPN and SSH authentication are separate layers.
2. Verify Tailscale before debugging SSH
Before touching SSH, verify that the network itself works.
Useful commands:
tailscale status
tailscale ping <hostname>
If tailscale ping succeeds in both directions, your network is healthy.
At that point, any remaining issue is almost certainly related to SSH configuration — including, as I later re-learned, whether sshd is even running (see section 14).
3. Windows does not install an SSH server by default
Windows ships with an SSH client, but not necessarily the server.
The server must be installed separately:
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0
This requires an elevated PowerShell session.
4. Installing isn’t enough
After installation:
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
Verify:
Get-Service sshd
5. Make sure something is actually listening
A quick sanity check:
netstat -ano | findstr :22
If nothing is listening on port 22, no amount of SSH client debugging will help.
6. Passwordless SSH uses public keys only
Never copy the private key anywhere.
The flow is:
Mac
------------------------
id_ed25519 (private)
id_ed25519.pub (public)
↓
Copy ONLY
id_ed25519.pub
↓
Windows
authorized_keys
The private key never leaves the client.
7. SSH debug output is incredibly useful
Instead of guessing, run:
ssh -vvv user@host
Look for:
Offering public key
This immediately tells you:
- which key is being used
- whether the server accepts it
- whether SSH falls back to password authentication
8. Multiple SSH keys can be confusing
Many developers have several keys:
id_ed25519
id_github
id_work
id_server
...
Without configuration, SSH may offer several keys.
A better approach is:
Host my-server
HostName 100.x.x.x
User username
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
This makes the behavior deterministic.
9. Windows has a special case for Administrator accounts
This was the most surprising discovery.
Windows OpenSSH ships with:
Match Group administrators
AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys
If your account belongs to the Administrators group, OpenSSH ignores:
~/.ssh/authorized_keys
and instead expects:
C:\ProgramData\ssh\administrators_authorized_keys
For personal machines, many people simply remove this block and use the standard per-user authorized_keys.
This was the biggest “gotcha” in the entire setup.
10. WSL inherits the Windows working directory
When connecting over SSH:
ssh windows-machine
then launching:
wsl
does not start in the Linux home directory.
Instead, it preserves the Windows working directory.
Example:
C:\Users\User
becomes
/mnt/c/Users/User
This is actually a nice feature once you know about it.
11. Start WSL in the Linux home directory
Instead of:
wsl
use:
wsl --cd ~
This immediately starts in:
/home/<user>
12. The nicest developer experience
Once everything is configured:
Mac
│
│ ssh
▼
Windows
│
▼
WSL Ubuntu
│
├── Java
├── Python
├── Rust
├── Docker
└── GPU development
It feels almost identical to developing on a remote Linux server while still having access to the Windows ecosystem.
13. What I’d do differently next time
If I were setting this up again:
- Install OpenSSH Server first.
- Verify
sshdis running. - Test password login.
- Configure SSH keys.
- Use
ssh -vvvimmediately if key authentication fails. - Check whether the Windows account belongs to the Administrators group.
- Configure an SSH alias with
IdentitiesOnly yes. - Configure WSL to start in the Linux home directory (or even make WSL the default shell for SSH sessions).
14. tailscale ping succeeding doesn’t mean sshd is running
Months after the initial setup, I hit this again: SSH over Tailscale simply failed to connect.
First instinct — re-check the network:
tailscale ping windows-machine
It succeeded, round trip and all. So the tailnet was healthy, the peer was reachable, and the connection wasn’t even going through a DERP relay. And yet SSH still refused to connect.
The actual cause: the sshd service on the Windows box was simply stopped. Fixed it the same way as section 4:
Get-Service sshd
Start-Service sshd
Set-Service -Name sshd -StartupType Automatic
Setting the startup type to Automatic matters — this is what prevents the service from silently staying down after the next reboot or Windows update.
Why did tailscale ping succeed at all if the SSH server was down?
Because tailscale ping doesn’t test SSH, or any application, at all. It operates purely at the tailnet/network layer:
Mac ──(WireGuard tunnel)──▶ Windows (tailscaled)
│
└── proves: this peer is up and reachable
over the tailnet (direct, or via DERP relay)
It’s answered entirely by the tailscaled daemon on the peer — it has zero visibility into which services or ports are listening on top of that network. A machine can have a perfectly healthy Tailscale connection while every single service on it (SSH, RDP, whatever) is stopped. This is really just section 1’s lesson again — Tailscale is only the network — showing up in a new, more confusing shape: it’s easy to assume a successful ping clears SSH of suspicion, but it only clears the network layer.
The takeaway / checklist for next time SSH-over-Tailscale fails:
tailscale ping <host>succeeds → network layer is fine, stop debugging Tailscale.- SSH still fails → check whether
sshdis actually running on the target, not just correctly configured:Get-Service sshd - If it’s
Stopped, start it and set it toAutomaticso it survives reboots:Start-Service sshd Set-Service -Name sshd -StartupType Automatic - Always double check
StartTypeis actuallyAutomaticafterwards — don’t just assume the command worked:Get-Service sshd | Select-Object Name, Status, StartTypeName Status StartType ---- ------ --------- sshd Running AutomaticIf
StartTypeever drifts back toManualorDisabled(e.g. after a Windows update), this is the step that catches it before it causes the next confusing outage.
15. Skip the wsl --cd ~ dance entirely — make WSL the default SSH shell
Section 11’s wsl --cd ~ workaround is fine, but it’s still a manual step you have to remember on every connection. There’s a better fix: tell OpenSSH to launch WSL itself as the login shell, so every SSH session lands directly inside WSL — no intermediate cmd.exe/PowerShell hop at all.
From an elevated PowerShell session on the Windows box:
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\wsl.exe" -PropertyType String -Force
Now:
ssh windows-machine
drops you straight into your default WSL distro’s shell, in its normal Linux home directory — the exact same effect as section 11’s wsl --cd ~, but automatic, for every session, forever.
Caveat worth knowing: DefaultShell isn’t limited to interactive logins — it becomes the shell OpenSSH uses for any command execution over that connection, including non-interactive ssh host <command> calls and tools that shell out over SSH (e.g. scp, some remote-exec tooling in editors). If anything in your workflow still expects a native Windows shell over SSH, that will now be routed through wsl.exe instead. For a personal dev box that’s basically always used as a WSL target, this tradeoff is a non-issue; for a mixed-use machine, weigh it first.
To revert to the default Windows shell:
Remove-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell
Final Thoughts
Most of the complexity wasn’t in Tailscale—it was in understanding how Windows’ OpenSSH implementation differs from the Linux/macOS defaults. Once configured, the experience is seamless: instant, passwordless SSH into a WSL environment over a secure private network, making remote development feel almost indistinguishable from working locally.
Tags: ssh, tailscale, windows, wsl, macos, remote_development