Passwordless SSH Login: Copying Keys to Servers
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
This guide shows how to log in to your servers with an SSH key instead of a password: how to put your public key on a server (one server or hundreds), how to make logins
short and easy with ~/.ssh/config, how to lock things down so a stolen key or a password guess is not enough, how to use keys in scripts and automation, and how to fix
"Permission denied (publickey)". It follows the guide "Creating SSH Keys", which shows how to make the key pair. If you have not made a key yet, do that first.
Practice on a test server before you change a production one. The most important safety rule of this whole guide: keep a second logged-in session open while you change SSH settings.
How it works
1. You have a key pair: the private key stays on your computer, the public key is one line of text
2. You add the PUBLIC key line to the file ~/.ssh/authorized_keys in the home directory of the account you want to log in to on the server
3. When you connect, the server sends a challenge. Your computer signs it with the private key, and the server checks the signature against the public keys in authorized_keys
4. If it matches, you are in. No password crosses the network. The private key never leaves your computer
Two things must exist: the public key installed in the RIGHT account on the server, and strict-enough permissions on the server side (SSH ignores keys in folders that others can write).
"Passwordless" means no SERVER password. You may still type a passphrase once to unlock the key locally (or nothing at all if the key is loaded in ssh-agent).
Before you start (checklist)
[ ] A key pair on your computer: ls ~/.ssh/id_ed25519 ~/.ssh/id_ed25519.pub (if missing: ssh-keygen -t ed25519 -C "you@laptop-2026")
[ ] You can log in to the server some other way right now (password, console, or another key). ssh-copy-id needs that once, to install the key
[ ] You know the login name and the address of the server (user@hostname or user@IP) and the SSH port (default 22)
[ ] The server's SSH service allows the method you use to copy (password login is usually on at first). Check with an admin if it is locked down
Confirm the server first, then the key:
ssh user@server.example.com # log in with your password once and check that you reach the right machine (type yes if it asks about the host key: see the fingerprint tip below)
Method 1: ssh-copy-id (the easy way, on Linux and macOS)
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server.example.com
It asks for your server password ONCE, then: creates ~/.ssh (mode 700) and authorized_keys (mode 600) if needed, appends your key (never duplicates it), and prints the number of keys added.
Output:
Number of key(s) added: 1
Now try logging into the machine, with: "ssh 'user@server.example.com'"
and check to make sure that only the key(s) you wanted were added.
Test it:
ssh user@server.example.com # no server password now (a passphrase prompt is only the local key unlock)
ssh -o PreferredAuthentications=publickey -o PasswordAuthentication=no user@server.example.com # PROVE the key is what got you in: fails with "Permission denied" if the key does not work
Useful options:
-i FILE The public key to install (otherwise it installs every key your agent holds, or the default .pub). Always pass -i to be exact
-p 2222 A non-default SSH port ssh-copy-id -i KEY.pub -p 2222 user@server
-o "ProxyJump bastion" Go through a jump host (or use -J bastion on newer versions) ssh-copy-id -i KEY.pub -o ProxyJump=alice@bastion.example.com user@10.0.0.20
-n DRY RUN: show which keys WOULD be added without adding them
-f Force: install even if it looks like the key is already there
-s Use the sftp subsystem instead of a shell (for servers with restricted shells)
-F ~/.ssh/config_alt Use another ssh config file
If you already set up ~/.ssh/config with a Host alias, ssh-copy-id understands it: ssh-copy-id -i ~/.ssh/id_ed25519.pub webserver
Warning: if the host key prompt appears ("The authenticity of host ... can't be established. ED25519 key fingerprint is SHA256:..."), do not just type yes. Compare the fingerprint with one you got from the server admin, the cloud console, or the provider's page.
Answering yes blindly to an attacker's server hands them your password.
Method 2: copy it by hand (no ssh-copy-id, or when it fails)
One command, works on any Unix-like system, with the same permissions logic:
cat ~/.ssh/id_ed25519.pub | ssh user@server 'umask 077; mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys'
Or step by step, so you can see each part:
1. On your computer: cat ~/.ssh/id_ed25519.pub # copy the whole ONE line
2. On the server (logged in as the target user):
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys # paste the line on its OWN line, save
chmod 600 ~/.ssh/authorized_keys
3. Test from your computer: ssh user@server
Use >> (append), never > (overwrite), or you erase the keys that are already there and lock other people out.
Each key must be ONE unbroken line. A pasted key that wrapped onto two lines will silently fail.
Restore the security labels on SELinux systems (RHEL, Fedora, Rocky, Alma): restorecon -R -v ~/.ssh
Windows (PowerShell) has no ssh-copy-id. Use:
type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh user@server "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
With PuTTY: create the key with PuTTYgen, copy the text from the box "Public key for pasting into OpenSSH authorized_keys file", and paste it into the server's authorized_keys as above. Use Pageant (or the Auth > Credentials private key setting) to log in.
Method 3: cloud servers and no password at all
Most clouds install your key when the machine is created, so you never need a password:
AWS EC2 Choose or import a key pair at launch (you can import your own ed25519 PUBLIC key: Key Pairs > Import). Login: ssh -i ~/.ssh/id_ed25519 ec2-user@HOST (ubuntu@ on Ubuntu images, admin@ on Debian)
Azure VM "SSH public key" during creation, or az vm user update --resource-group RG --name VM --username azureuser --ssh-key-value "$(cat ~/.ssh/id_ed25519.pub)" (see the free VM guides)
Google Cloud Project or instance metadata ssh-keys or OS Login: gcloud compute ssh VM --zone ZONE (adds your key for you)
DigitalOcean, Linode, Hetzner Add your public key in the account's SSH Keys page, then tick it when you create a droplet or server
cloud-init Put the key in user-data so every new server gets it:
#cloud-config
users:
- name: alice
groups: sudo
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:ALL
ssh_authorized_keys:
- ssh-ed25519 AAAAC3Nza...your-key... alice@laptop-2026
Provisioning tools (Terraform, Ansible, Packer) can install keys the same way: see "Many servers" below.
Physical servers and VMs without SSH yet: use the console (iDRAC/iLO, hypervisor console) to create ~/.ssh/authorized_keys, or install the key during OS setup.
Many servers at once
A simple loop (asks for each server's password once; use a list in a file):
for h in web1 web2 db1 db2; do ssh-copy-id -i ~/.ssh/id_ed25519.pub "alice@$h.example.com"; done
while read -r h; do ssh-copy-id -i ~/.ssh/id_ed25519.pub "alice@$h"; done < servers.txt
Same password on all servers, non-interactively (sshpass is handy but puts a password in the process list or a file: use it only for the one-time bootstrap on a trusted machine):
sudo apt install sshpass
sshpass -f ~/.bootstrap_pw ssh-copy-id -o StrictHostKeyChecking=accept-new -i ~/.ssh/id_ed25519.pub alice@web1.example.com (keep ~/.bootstrap_pw at mode 600, delete it afterwards)
Ansible (recommended for fleets): the authorized_key module adds or removes keys idempotently, and can fix permissions.
- name: Install admin SSH key
hosts: all
become: true
tasks:
- name: Add alice's public key
ansible.posix.authorized_key:
user: alice
state: present
key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
exclusive: false # true would REMOVE every other key in the file: powerful, so use carefully
ansible-playbook -i inventory.ini keys.yml --ask-pass --ask-become-pass (the first run needs a password; later runs use the key)
Parallel and other tools: pssh / parallel-ssh, pdsh, Fabric, and the configuration tools (Puppet ssh_authorized_key, Salt ssh_auth). For large teams use SSH certificates or a central directory (see the "Creating SSH Keys" guide) so you do not manage keys per server.
Servers behind a bastion: ssh-copy-id -i ~/.ssh/id_ed25519.pub -o ProxyJump=alice@bastion.example.com alice@10.0.0.20
Different users on the server: install the key for EACH account you need (root, deploy, appuser). Each account has its own ~/.ssh/authorized_keys.
Add more than one key, or another person
authorized_keys holds one public key per line, so many people and devices can log in to one account. Add a comment at the end of each line so you know whose it is:
ssh-ed25519 AAAAC3... alice@laptop-2026
ssh-ed25519 AAAAC3... bob@desktop-2026
ssh-rsa AAAAB3... ci-runner-deploy
Better: give each person their OWN account on the server instead of sharing one, so logs and sudo show who did what.
Remove a key: sed -i '/alice@laptop-2026/d' ~/.ssh/authorized_keys (delete the line by its comment) or edit the file
See who has access: ssh-keygen -l -f ~/.ssh/authorized_keys (one fingerprint per key with its comment)
On a server run as root to audit every account: for f in /home/*/.ssh/authorized_keys /root/.ssh/authorized_keys; do echo "== $f"; ssh-keygen -l -f "$f" 2>/dev/null; done
Restrict what a key may do (authorized_keys options)
Options go at the START of the key's line, separated by commas. They limit damage if the key leaks, and they suit automation keys.
from="203.0.113.10,10.0.0.0/24" ssh-ed25519 AAAAC3... backup-server only accept this key from those addresses
command="/usr/local/bin/backup.sh",no-pty,no-port-forwarding ssh-ed25519 AAAA... backup run ONLY this command, whatever the client asks for
restrict,command="/usr/bin/rrsync -ro /data" ssh-ed25519 AAAA... rsync-reader a key that may only read /data with rsync (the rrsync helper ships with rsync: its path varies by distribution)
no-agent-forwarding,no-X11-forwarding,no-port-forwarding,no-pty ssh-ed25519 AAAA... a login that can run commands but nothing else
expiry-time="20261231" ssh-ed25519 AAAA... the key stops working after that date (OpenSSH 7.7+)
restrict shortcut that turns off all optional features (pty, forwarding, agent); add back only what is needed, e.g. restrict,pty
principals=, cert-authority used with SSH certificates
An automation key with from=,command= and restrict is far safer than a general-purpose key with no passphrase.
Make logins short: ~/.ssh/config
Instead of ssh -i ~/.ssh/id_work -p 2222 alice@web1.prod.example.com you type ssh web1. File: ~/.ssh/config (chmod 600)
Host web1
HostName web1.prod.example.com
User alice
Port 2222
IdentityFile ~/.ssh/id_work
IdentitiesOnly yes # offer ONLY this key (avoids "Too many authentication failures")
Host *.prod.example.com
User alice
IdentityFile ~/.ssh/id_work
IdentitiesOnly yes
ServerAliveInterval 60 # keep idle connections from dropping
ServerAliveCountMax 3
Host db-internal
HostName 10.0.0.20
User alice
ProxyJump bastion # go through a jump host (safer than agent forwarding)
Host bastion
HostName bastion.example.com
User alice
IdentityFile ~/.ssh/id_work
Host *
AddKeysToAgent yes # load keys into the agent when first used
ForwardAgent no # NEVER by default
HashKnownHosts yes # hide host names in known_hosts
ControlMaster auto # reuse one connection for many sessions (fast repeated logins and scp)
ControlPath ~/.ssh/cm-%r@%h:%p
ControlPersist 10m
Rules: the FIRST matching value wins, so put specific Host blocks before the general Host * block. Check what SSH will use: ssh -G web1 | head -20 Test with details: ssh -v web1
Split big configs: Include ~/.ssh/config.d/* at the top of ~/.ssh/config.
Also convenient: scp file web1:/tmp/ rsync -av dir/ web1:/srv/ ssh web1 uptime ssh -L 8080:localhost:80 web1 (a tunnel) sftp web1
Lock the door: turn off password logins on the server
Only after key login works and you have tested it in a SECOND terminal. Passwords can be guessed by bots all day. Keys cannot.
1. Keep your current session open. Open a second terminal and confirm: ssh -o PasswordAuthentication=no user@server works
2. Edit the server's SSH settings. On modern Debian, Ubuntu and RHEL, put your changes in a drop-in file (files in /etc/ssh/sshd_config.d/ are read first; the first value wins, so use a low number):
sudo nano /etc/ssh/sshd_config.d/00-keys-only.conf
PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no (older name: ChallengeResponseAuthentication no)
PermitRootLogin prohibit-password (or "no" and use a normal user with sudo: best)
AuthenticationMethods publickey (optional: keys only)
MaxAuthTries 3
LoginGraceTime 30
X11Forwarding no
AllowUsers alice bob deploy (optional: only these accounts may log in. Or AllowGroups sshusers )
3. Check the syntax, then reload (reload keeps existing sessions):
sudo sshd -t && sudo systemctl reload ssh (RHEL family: sshd)
4. Check the settings that are REALLY in effect (other files can override):
sudo sshd -T | grep -iE 'passwordauthentication|pubkeyauthentication|permitrootlogin|kbdinteractive|authenticationmethods'
5. From a third terminal, prove a password login is now refused: ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no user@server -> "Permission denied (publickey)"
6. Only then close your safety session.
Cloud images often ship a file /etc/ssh/sshd_config.d/50-cloud-init.conf that sets PasswordAuthentication yes . Because the first value read wins, your file must come first alphabetically (00-...) or you must edit that file too.
If you get locked out: use the provider's serial or web console, or attach the disk to another VM, and fix sshd_config or authorized_keys.
Extras: fail2ban (sudo apt install fail2ban) bans IPs that keep failing. Changing the SSH port only reduces log noise: it is not real security. Consider a firewall that limits port 22 to known networks (ufw allow from 203.0.113.0/24 to any port 22).
Passwordless SSH is not passwordless sudo. Logging in and using sudo are separate. If you want sudo without a password for a service account, add it deliberately in /etc/sudoers.d/ with visudo, and limit it to specific commands.
Keys in scripts, cron and automation
Goal: a script or scheduled job that connects to other servers without a human typing anything.
Safest pattern:
1. Create a DEDICATED key for the job (not your personal key), and a dedicated account on the target:
ssh-keygen -t ed25519 -N "" -C "backup-job@backupserver" -f ~/.ssh/id_backup (the empty passphrase is the trade-off. Protect the file: mode 600, owned by the job's user)
2. Install it with restrictions on the TARGET so a leaked key can only do the job:
from="203.0.113.5",command="/usr/local/bin/backup-receive.sh",restrict ssh-ed25519 AAAA... backup-job@backupserver
3. Use these options in the script so it never waits for a question or hangs:
ssh -i ~/.ssh/id_backup -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 backup@target 'command'
BatchMode=yes never prompt for a password or passphrase (fail instead)
StrictHostKeyChecking=accept-new trust a host the FIRST time, and refuse if it changes later (safer than "no")
ConnectTimeout=10 do not hang forever
4. Pre-load the server's host key so the first run is not blind: ssh-keyscan -t ed25519 target.example.com >> ~/.ssh/known_hosts (compare the fingerprint with a trusted source first)
Alternatives to keys without passphrases: a passphrase-protected key loaded into an agent that starts at boot (keychain or a systemd user agent), short-lived SSH certificates, or a secrets manager (Vault, cloud KMS).
Cron caveat: cron has no agent and a tiny environment. Use full paths, ssh -i, and write output to a log: */15 * * * * /usr/local/bin/pull-data.sh >> /var/log/pull-data.log 2>&1
CI/CD: store the private key as an encrypted secret, load it with ssh-agent for the job only, add the host to known_hosts, and use a deploy key with from= and command= where possible.
Git over SSH works the same way: upload the PUBLIC key to GitHub/GitLab (Settings > SSH keys), then ssh -T git@github.com (should greet you by name). A repo-specific read-only deploy key is the right choice for servers that only need to pull.
Removing and replacing keys (offboarding, rotation, a lost laptop)
Remove one key from one server: sed -i '/alice@laptop-2026/d' ~/.ssh/authorized_keys
Remove it everywhere (Ansible):
- ansible.posix.authorized_key: { user: alice, key: "{{ lookup('file', 'old.pub') }}", state: absent }
Rotate a key: 1) create the new key 2) install the new public key on all servers (keep the old one) 3) test the new key 4) remove the old public key everywhere 5) delete the old private key
A lost or stolen laptop: assume the private key is exposed unless the disk is encrypted AND the key has a strong passphrase. Remove that key from every server, GitHub and cloud console first, then investigate logins in /var/log/auth.log or the journal.
Departing staff: remove their keys and their accounts, and check for keys they added to shared accounts (audit authorized_keys files, service accounts and cron jobs).
Troubleshooting: "Permission denied (publickey)"
Start on your computer with detail: ssh -v user@server (or -vv, -vvv for more). Read what it says:
"Offering public key: /home/alice/.ssh/id_ed25519 ED25519 SHA256:..." your client tried this key (if you never see "Offering", the key is not being offered: fix IdentityFile, the agent (ssh-add -l), or the file path)
"Server accepts key: ..." GOOD: the server accepted the public key
"Authentications that can continue: publickey" the server wants a key and did not accept yours
Then the server. Read its log (needs access, so use another session or the console):
sudo journalctl -u ssh -e or sudo tail -f /var/log/auth.log (RHEL family: sudo tail -f /var/log/secure )
Look for: "Authentication refused: bad ownership or modes for directory /home/alice" (fix permissions) "Failed publickey for alice" "userauth_pubkey: key type ssh-rsa not in PubkeyAcceptedAlgorithms"
Checklist:
1. Right USER? ssh alice@server vs root@server. The key must be installed in THAT account
2. Right key? Does the fingerprint match? ssh-keygen -lf ~/.ssh/id_ed25519.pub and on the server ssh-keygen -lf ~/.ssh/authorized_keys
3. Permissions on the server (as the target user): chmod 700 ~/.ssh ; chmod 600 ~/.ssh/authorized_keys ; chmod go-w ~ ; and the files must be OWNED by that user ( chown -R alice:alice ~/.ssh )
4. A broken line? authorized_keys lines must be ONE line each, with no line breaks, no extra characters, and no Windows line endings (dos2unix ~/.ssh/authorized_keys)
5. The home directory itself: /home/alice must be owned by alice and not writable by group or others. A group-writable home directory breaks key login (StrictModes)
6. sshd config: PubkeyAuthentication yes; check AuthorizedKeysFile (default .ssh/authorized_keys); check AllowUsers/AllowGroups/DenyUsers and Match blocks: sudo sshd -T | grep -iE 'pubkey|authorizedkeys|allowusers|allowgroups'
7. SELinux (RHEL family): restorecon -R -v ~/.ssh and sudo ausearch -m avc -ts recent | grep sshd
8. Account locked or has no valid shell: sudo passwd -S alice (status L = locked password. On some systems a locked account also refuses key logins) getent passwd alice (the shell must not be nologin)
9. Key algorithm not accepted: a very old client vs new server, or an old RSA (SHA-1) key vs a new server. Use an ed25519 key. Check: ssh -Q PubkeyAcceptedAlgorithms (on the client) and sshd -T | grep pubkeyaccepted (on the server)
10. Too many keys offered: "Too many authentication failures". Use ssh -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 user@server or set IdentitiesOnly in ~/.ssh/config
11. Root login blocked: PermitRootLogin no (the key can be perfect, root is still refused). Log in as a normal user and use sudo
12. Debug the server with a temporary second sshd on another port (never on your only access): sudo /usr/sbin/sshd -d -p 2222 then ssh -p 2222 -v user@server
Other messages:
"Enter passphrase for key ...:" then success That is the LOCAL key unlock. Normal. Use ssh-agent to avoid it
It still asks for the SERVER password ("alice@server's password:") The key was not accepted, so SSH fell back. Use the checklist above. Add -o PasswordAuthentication=no to see the real error
"WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!" The server's host key changed: a rebuilt server, or an attack. Verify with the admin, then ssh-keygen -R server.example.com and reconnect
"Host key verification failed" Same family: known_hosts mismatch. Fix as above, or StrictHostKeyChecking=accept-new for automation
"ssh-copy-id: ERROR: No identities found" No key found. Run ssh-keygen, or pass -i path/to/key.pub
"ssh-copy-id: WARNING: All keys were skipped because they already exist on the remote system" The key is already installed. Test the login. If it still fails, the problem is elsewhere (permissions, sshd config)
"Connection refused" / "timed out" sshd is not running, wrong port, or a firewall. Try nc -zv server 22 , sudo systemctl status ssh on the server, and check security groups and ufw
"Connection closed by ... port 22" right after connecting The server closed it: fail2ban ban, TCP wrappers, MaxStartups, AllowUsers rules, or the account is not allowed
"Permission denied (publickey,password)" (password also fails) The user or password is wrong, or the account is restricted. Check /var/log/auth.log
Everything worked yesterday The server was rebuilt, the key was removed by config management, permissions changed by an update or a script, or your agent lost the key (ssh-add -l)
Quick reference
ssh-keygen -t ed25519 -C "you@laptop-2026" Make a key (see the first guide)
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server Install the key
cat ~/.ssh/id_ed25519.pub | ssh user@server 'umask 077; mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys' The manual way
ssh -o PreferredAuthentications=publickey -o PasswordAuthentication=no user@server Test that the KEY works
chmod 700 ~/.ssh ; chmod 600 ~/.ssh/authorized_keys ; chmod go-w ~ Server-side permissions
sudo sshd -t && sudo systemctl reload ssh ; sudo sshd -T | grep -i passwordauth Apply and verify SSH server settings
PasswordAuthentication no ; PermitRootLogin prohibit-password ; PubkeyAuthentication yes Keys only
ssh -v user@server See exactly why a login fails
ssh-keygen -R server ; ssh-keygen -lf ~/.ssh/authorized_keys ; sed -i '/comment/d' ~/.ssh/authorized_keys Manage host and user keys
Tips
Test the key login BEFORE you turn passwords off, and keep a second session open while you change sshd settings. Run sshd -t before every reload.
Use ssh-copy-id -i KEY.pub user@host. It sets the right folder and file permissions for you and never overwrites existing keys.
If SSH still asks for the server password, the key was not accepted. Run ssh -v, then check permissions on the server (700, 600, home not group-writable) and the server log.
Confirm the host fingerprint on first connection instead of typing yes without looking.
Use ~/.ssh/config with Host blocks, IdentityFile and IdentitiesOnly so you type ssh web1 instead of long commands. Use ProxyJump instead of agent forwarding.
For servers at scale, use Ansible (authorized_key), cloud-init or your cloud's key management. Give each person their own account and key, and offboard by removing the key.
For automation, use a dedicated key restricted with from=, command= and restrict, plus BatchMode=yes in scripts.
Turn off password logins and root password login once keys work, check the effective settings with sshd -T, and add a firewall or fail2ban.
Rotate keys regularly, remove keys when people leave or laptops are lost, and audit authorized_keys files now and then with ssh-keygen -l.
Remember: a passphrase prompt is your local unlock, and the private key never leaves your computer. If the private key is exposed, replace it everywhere.