Every time a fresh machine comes up on my network, the first thing I want is my laptop's key in its authorized_keys file. On Linux or macOS that is one command you have known for years:
ssh-copy-id root@192.0.2.20On Windows, from PowerShell, that command does not exist. Not "is not on your PATH." Does not exist.
ssh-copy-id : The term 'ssh-copy-id' is not recognized as the name of a cmdlet,
function, script file, or operable program.Windows has shipped a real OpenSSH client since Windows 10, and it is genuinely good. Here is everything in it:
scp.exe sftp.exe ssh.exe ssh-add.exe ssh-agent.exe
ssh-keygen.exe ssh-keyscan.exe ssh-pkcs11-helper.exe ssh-sk-helper.exeNine binaries. No ssh-copy-id. The reason is that ssh-copy-id was never a binary. It is a POSIX shell script that ships alongside OpenSSH, and Microsoft's port compiles the C programs rather than carrying the shell scripts. So the omission is structural rather than an oversight, which also means it is not going to appear in a future update.
Do the same job in one line
The script does something simple: read your public key, connect, create the directory, append the key, set the permissions. You can do all of it inline.
Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" | ssh root@192.0.2.20 `
"mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"You type the password once, in your own terminal, and never again for that host.
Two Windows-specific things that trip people up in that line. The path is $env:USERPROFILE\.ssh\, because ~/.ssh is not a thing PowerShell expands the way a POSIX shell does. And the backtick is PowerShell's line continuation character, not a backslash.
If you would rather not pipe text between a Windows program and a remote shell at all, this form avoids the whole class of line-ending and encoding questions by passing the key as an argument:
$key = (Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" -Raw).Trim()
ssh root@192.0.2.20 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && printf '%s\n' '$key' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"This is the one I use, because it is explicit about the trailing newline instead of hoping the pipeline gets it right.
Or use the copy you already have
If you have Git for Windows installed, you already have the real script. Git Bash bundles the MSYS2 build of OpenSSH, which includes the shell scripts the Windows port leaves out:
$ command -v ssh-copy-id
/usr/bin/ssh-copy-idSo open Git Bash instead of PowerShell and ssh-copy-id root@192.0.2.20 works exactly as it does on Linux. Two different SSH clients live on the same machine, one of which has the command and one of which does not, which is a good thing to know before you spend twenty minutes deciding your installation is broken.
When the key lands and login still asks for a password
This is the failure that actually costs time, because everything looks correct. The key is in the file, the file is on the server, and the server still wants a password.
Almost always it is permissions. sshd runs with StrictModes on by default, and it will refuse a key file that anyone other than the owner can write to. That includes the home directory itself, not just .ssh.
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chmod go-w ~ # the one people misssshd will not tell you this on the client side. It just falls through to the next authentication method, which looks identical to the key not being there. The server knows exactly what happened:
tail /var/log/auth.log
Authentication refused: bad ownership or modes for directory /rootRun ssh -v on the client and you will see it offer the key and get nothing back, which at least tells you the problem is on the far end.
The other common cause is having several keys. The client offers them in order, and if the server's MaxAuthTries limit is reached before it gets to the right one, you are refused for having too many keys rather than the wrong one. Pin it explicitly:
Host newnode
HostName 192.0.2.20
User root
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yesIdentitiesOnly yes is the important line. Without it, the client will still offer everything in your agent regardless of what you specified.
Why I bother with any of this
The point is not saving keystrokes. It is that the password gets typed exactly once, locally, by me, on the machine it belongs to. It never goes into a note, a chat window, a script or a config file. After that first connection the credential for that host is a key on my laptop protected by the operating system, and adding a new machine to my network is a thirty second job rather than a decision about where to write a password down.
That is a small habit that compounds. Every host I have built in the last year got exactly this treatment on its first boot, and I have never had to go looking for one of their passwords, because for most of them I do not remember what it was.
A note on the addresses
Addresses in this post use the range reserved for documentation under RFC 5737.
Resources
- Microsoft OpenSSH for Windows documentation - what ships and what does not
- sshd_config StrictModes - why permissions silently break key auth
- More homelab write-ups: iamkay.eu/blog

