Guides

ssh-copy-id: copy your SSH key to a server, plus the Windows equivalent

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host appends your public key to authorized_keys on the server. All options, a Windows PowerShell version and fixes.

Published ~9 min read

Short answer: ssh-copy-id -i ~/.ssh/id_ed25519.pub USER@SERVER_IP copies your public key into ~/.ssh/authorized_keys on the server, so from then on you log in with the key instead of a password. It needs one working login to do that, normally your password, and it skips keys that are already installed. Windows has no ssh-copy-id; a PowerShell line that does the same job is further down.

Linux · macOS
ssh-copy-id -i ~/.ssh/id_ed25519.pub USER@SERVER_IP

Replace USER with the account you want to log in as (often root on a new VPS) and SERVER_IP with the server’s address. ssh-copy-id is part of the OpenSSH client on Linux and ships with macOS. What follows was checked against the script itself in OpenSSH 9.2 (Debian 12) and the man page of newer releases.

Step 1: create a key if you do not have one

Look for an existing key first: ls ~/.ssh/*.pub. If that lists id_ed25519.pub, skip to step 2. Otherwise create an Ed25519 key on your own computer, never on the server:

Linux · macOS · Windows PowerShell
ssh-keygen -t ed25519 -C "you@laptop"

-C only sets the comment at the end of the public key, so you can tell your keys apart in authorized_keys later; use any label. Press Enter to accept the default file, then set a passphrase. Spell out -t ed25519: OpenSSH made Ed25519 the default in 9.5, but older versions, including Debian 12’s 9.2, still create RSA keys when you leave it out.

Example output
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/you/.ssh/id_ed25519):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/you/.ssh/id_ed25519
Your public key has been saved in /home/you/.ssh/id_ed25519.pub
The key fingerprint is:
SHA256:nzRUX6Jlgiu7/hcjnTQRyqk+qs3nGDVZ/mIZS1pw4OM you@laptop

id_ed25519 is the private key and stays on your computer. id_ed25519.pub is a single line starting with ssh-ed25519, and only that file goes to servers.

Step 2: copy the key with ssh-copy-id

Linux · macOS
ssh-copy-id -i ~/.ssh/id_ed25519.pub USER@SERVER_IP
Example output
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/you/.ssh/id_ed25519.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
alice@203.0.113.10's password:

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'alice@203.0.113.10'"
and check to make sure that only the key(s) you wanted were added.

That is the wording of OpenSSH 9.2; newer releases phrase the last lines a little differently. On the first connection to a server, ssh asks you to confirm its host key before any of this; answer yes only for the server you meant to reach. Behind that output, the script does three things:

  • It picks the keys. With -i, only that file; a name without .pub gets it appended. Without -i, it installs every key your ssh-agent holds (whatever ssh-add -L lists), and only when the agent is empty does it fall back to the newest ~/.ssh/id*.pub. That is why -i is worth typing every time.
  • It filters. It tries to log in with each key, key-only, and drops the ones that already work. With -i, the script insists on the private key sitting next to the .pub file for this step; -f skips the check and that requirement.
  • It installs. It logs in once more, which is where the password prompt comes from, changes to your home directory and runs umask 077, mkdir -p .ssh, adds a line break if the last line of authorized_keys lacks one, appends the key and, where SELinux tools are installed, runs restorecon on both paths.

The umask 077 means a .ssh directory it creates gets mode 700 and a new authorized_keys gets 600. It does not repair a directory or file that already exists with the wrong mode or owner; that is the most common reason a key is installed and still ignored (see below).

ssh-copy-id options

OptionWhat it doesExample
-i FILEInstall only the keys in FILE; .pub is appended if missingssh-copy-id -i ~/.ssh/id_ed25519.pub USER@SERVER_IP
-p PORTSSH port, lowercase as in ssh (scp uses -P)ssh-copy-id -p 2222 -i ~/.ssh/id_ed25519.pub USER@SERVER_IP
-o OPTIONAny ssh -o option, passed throughssh-copy-id -i ~/.ssh/id_ed25519.pub -o PubkeyAuthentication=no USER@SERVER_IP
-F FILEUse another ssh config filessh-copy-id -F ./ssh_config myvps
-nDry run: prints the keys it would add. Still logs in to checkssh-copy-id -n -i ~/.ssh/id_ed25519.pub USER@SERVER_IP
-fForce: skip the check, no private key needed; can add duplicatesssh-copy-id -f -i ./colleague.pub USER@SERVER_IP
-sSFTP mode for accounts that may only use SFTPssh-copy-id -s -i ~/.ssh/id_ed25519.pub USER@SERVER_IP
-t FILEWrite to FILE instead of .ssh/authorized_keys; OpenSSH 9.4+ only (Ubuntu 24.04, Debian 13)ssh-copy-id -t .ssh/authorized_keys2 -i ~/.ssh/id_ed25519.pub USER@SERVER_IP
-xTrace the script's commands; OpenSSH 9.4+ onlyssh-copy-id -x -i ~/.ssh/id_ed25519.pub USER@SERVER_IP

-f is also how you install somebody else’s public key for them: you have their .pub file but not their private key, so the filter step cannot run. Put -f before -i. The script looks for the matching private key the moment it reads -i, so ssh-copy-id -i ./colleague.pub -f stops with ERROR: failed to open ID file './colleague'.

ssh-copy-id with a non-standard port

ssh-copy-id passes -p straight to ssh, so it is lowercase: ssh-copy-id -p 2222 -i ~/.ssh/id_ed25519.pub USER@SERVER_IP. A host alias from ~/.ssh/config works too, port and user included: ssh-copy-id -i ~/.ssh/id_ed25519.pub myvps. If you are not sure which port the server uses, SSH port 22 shows how to check from both sides. Moving SSH to another port without locking yourself out is covered in change the SSH port safely.

ssh-copy-id on Windows

The OpenSSH client that comes with Windows includes ssh, scp, sftp, ssh-keygen, ssh-agent, ssh-add and ssh-keyscan, but not ssh-copy-id, which is a shell script. For a Linux server, this PowerShell line does the same job:

PowerShell (Windows PowerShell 5.1 or PowerShell 7)
Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" | ssh USER@SERVER_IP "umask 077; mkdir -p .ssh && { tail -c1 .ssh/authorized_keys 2>/dev/null | grep -q . && echo; tr -d '\r'; } >> .ssh/authorized_keys"

Enter the server password when asked. The part in double quotes runs on the server and copies what ssh-copy-id does: umask 077 so new files get 700 and 600, and a line break before the key if the file’s last line has none. tr -d '\r' removes the carriage return Windows can put at the end of the piped line. The string contains no $ and no inner double quotes, so Windows PowerShell 5.1 and PowerShell 7 hand it to ssh unchanged. We ran the server-side part under bash and dash against a missing .ssh, a file with a trailing newline and one without; each time it produced a clean file with the right modes.

If your key has another name, change the path in Get-Content. Keys made with ssh-keygen on Windows live in $env:USERPROFILE\.ssh; a key from PuTTYgen has to be in OpenSSH format, which is the text in its box labelled for pasting into an OpenSSH authorized_keys file.

Copy the key by hand

When you cannot pipe anything, for example from a locked-down machine, print the public key (cat ~/.ssh/id_ed25519.pub, or Get-Content on Windows), copy the line and run this on the server, logged in as the user the key is for:

on the server · Debian · Ubuntu
mkdir -p ~/.ssh && chmod 700 ~/.ssh
touch ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
[ -z "$(tail -c1 ~/.ssh/authorized_keys)" ] || echo >> ~/.ssh/authorized_keys
echo 'PUBLIC_KEY_LINE' >> ~/.ssh/authorized_keys

Replace PUBLIC_KEY_LINE with the whole line from the .pub file, starting with ssh-ed25519. Keep the single quotes. The third line is the check ssh-copy-id makes: if the file’s last line has no line break, it adds one, so the new key does not get glued onto the end of an old one and break both. To check the result, run ssh-keygen -l -f ~/.ssh/authorized_keys on the server and ssh-keygen -l -f ~/.ssh/id_ed25519.pub on your computer: the fingerprints must match. A line that got broken while pasting is skipped silently, so it simply does not appear in the list.

On a VMHeaven Ubuntu or Debian VPS you can skip all of this: paste the public key at checkout and it is installed when the server is built, while password login stays available as a fallback.

Step 3: test the key before you turn passwords off

Log in with the key only, so a silent fallback to the password cannot fool you:

Linux · macOS · Windows PowerShell
ssh -o PreferredAuthentications=publickey -i ~/.ssh/id_ed25519 USER@SERVER_IP

If you get a shell without being asked for the account password (the key’s own passphrase does not count), the key works. With -v added, the log says Authenticated to SERVER_IP ([SERVER_IP]:22) using "publickey". If it says Permission denied (publickey) instead, fix that first.

ssh-copy-id: Permission denied and other errors

Permission denied (publickey) while running ssh-copy-id

Example output
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
alice@203.0.113.10: Permission denied (publickey).

No password prompt appeared, so the server does not accept passwords for this account. Password login may already be off (on the server, sudo sshd -T | grep -i passwordauthentication shows the effective value, including files in /etc/ssh/sshd_config.d/), or the account has no password at all. ssh-copy-id needs one login that works. Either run it from a computer whose key is already accepted, or add the key by hand from the server’s console. In the VMHeaven panel the VNC console has a field that sends pasted text, so you do not have to type the long key line.

If the password prompt does appear but rejects a password you know is right, look at the user. sshd’s default for root is PermitRootLogin prohibit-password, which accepts keys only, and a self-installed Debian or Ubuntu keeps that default. Copy the key to a normal account with sudo rights, or add it to /root/.ssh/authorized_keys from the console.

The key is installed, but ssh still asks for a password

sshd ignores authorized_keys when it, the .ssh directory or the home directory can be written by anyone but the owner. Its log says so:

on the server · Debian · Ubuntu
sudo journalctl -u ssh --since "10 minutes ago" | grep -i -E 'refused|bad ownership'
Example output
Authentication refused: bad ownership or modes for directory /home/alice

Fix the modes, logged in as the user the key is for:

on the server · Debian · Ubuntu
chmod go-w ~
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys

If ls -la ~/.ssh shows the files owned by root, usually because they were created with sudo, that user cannot change them. Hand them over from a root shell, with the user’s name in place of USERNAME:

on the server · Debian · Ubuntu
sudo chown -R USERNAME: /home/USERNAME/.ssh

On Rocky Linux and AlmaLinux the unit is sshd, not ssh. If the modes are right and it still fails, check the other usual suspects:

  • Wrong user. A key in /root/.ssh/authorized_keys does nothing for ssh deploy@SERVER_IP. Run ssh-copy-id with the user you log in as.
  • Keys are read from elsewhere. sudo sshd -T | grep -i authorizedkeysfile shows where sshd looks. The default is .ssh/authorized_keys .ssh/authorized_keys2; ssh-copy-id always writes the first unless you pass -t (OpenSSH 9.4 and later).
  • Root login is limited. PermitRootLogin no refuses root even with a valid key.

If the numbers 700 and 600 mean nothing to you, the chmod calculator translates them into read, write and execute bits for owner, group and others.

The full checklist, including ssh -v output to read on the client side, is in Permission denied (publickey).

WARNING: All keys were skipped because they already exist

The filter step logged in with the key, so ssh-copy-id assumes it is installed. That is usually true. It can also happen when another key that already works was offered during the check, for example one set with IdentityFile for that host in ~/.ssh/config. Add -f to install without the check.

ERROR: No identities found

There is no key in your agent and no ~/.ssh/id*.pub file. Create one as in step 1, or point at the right file with -i.

Too many authentication failures

Your ssh-agent holds so many keys that the server gives up before the password prompt. Add -o PubkeyAuthentication=no so the install login goes straight to the password; why it happens is explained in Too many authentication failures.

Frequently asked

Is there an ssh-copy-id for Windows?

Not as a command: the OpenSSH client built into Windows ships ssh, scp and ssh-keygen but no ssh-copy-id. From PowerShell, pipe the public key over ssh and append it to ~/.ssh/authorized_keys on the server with umask 077, or paste the key into that file by hand.

How do I use ssh-copy-id with a different SSH port?

Add lowercase -p, as with ssh: ssh-copy-id -p 2222 -i ~/.ssh/id_ed25519.pub user@host. scp is the odd one out with capital -P; ssh-copy-id passes -p straight to ssh.

Why does ssh-copy-id fail with Permission denied (publickey)?

The server does not accept a password for that account, so ssh-copy-id has no way to log in and install the key. Run it from a computer whose key is already accepted, or append the key to ~/.ssh/authorized_keys from the server's console.

Related articles

All guides