Skip to content
← All articles

SSH Key Authentication: Generate, Install and Disable Password Login

In short. SSH key authentication replaces a password with a file pair: the private key never leaves your machine, the public key goes into ~/.ssh/authorized_keys on the server. Generate a key with ssh-keygen -t ed25519, install it with ssh-copy-id, set 700 on the directory and 600 on the file, verify login in a second session, and only then disable PasswordAuthentication.

How SSH key authentication actually works

A password is a shared secret: it exists on both sides and can be brute-forced. An SSH key works differently. You generate two files:

  • id_ed25519 — the private key. It never leaves your machine. It is your access.
  • id_ed25519.pub — the public key. Safe to copy, paste into a chat, or install on a dozen servers.

On connection the server sends a random challenge. The client signs it with the private key, and the server verifies the signature against the public key stored in authorized_keys. The private key is never transmitted — in any form. So traffic interception, a compromised jump host, or a leaked provider password database gives an attacker nothing.

The practical effect: the bots hammering root:123456 on port 22 around the clock stop being a threat category. Not "less likely to succeed" — mathematically unable to.

A private key is not "the server password", it is the access itself. Never send id_ed25519 over a messenger, drop it into shared storage, commit it to a repository, or bake it into a Docker image. If it leaks, the key is revoked — you do not "change the password".
Diagram of SSH key authentication: the private key on the client signs the server challenge, the public key in authorized_keys verifies the signature
The private key never travels over the wire — the server only verifies a signature.

ed25519 vs RSA: which SSH key type to choose

Short answer: ed25519. It landed in OpenSSH 6.5 and has been the sane default ever since.

  • Strength. Ed25519 provides roughly 128 bits of security at a 256-bit key length. Matching that with RSA requires at least 3072 bits.
  • Size. An ed25519 public key is a single short line that fits in a terminal. An RSA 4096 public key is a multi-line wall of base64 that is painful to copy by hand.
  • Speed. Signing and verification are noticeably faster than RSA 4096 — measurable on CI and mass deployments.
  • Fewer ways to get it wrong. No curve to pick, no signature strength that silently depends on the quality of the client's random number generator.

Keep an RSA key only as a compatibility fallback: legacy network gear, appliances, or a Git host that does not speak Ed25519.

Key typeStrengthPublic key sizeSupportVerdict
ed25519~128 bits, modern Curve25519One line, ~80 charactersOpenSSH 6.5+, GitHub, GitLab, every current distributionDefault choice
ed25519-skSame, plus hardware presence checkOne line, slightly longerNeeds a FIDO2 token and recent OpenSSH on both endsBest option for admin keys
RSA 4096Adequate at 3072 bits and above; 1024 and 2048 are weakSeveral lines, hundreds of charactersPractically universal, including legacyCompatibility fallback
ECDSA (nistp256/384/521)Nominally comparable to ed25519ShortWideWorks, but no reason to pick it today
DSA (ssh-dss)Capped at 1024 bits, considered obsoleteMediumDisabled by default since OpenSSH 7.0, removed from recent releasesDo not use

One separate trap: the ssh-rsa signature algorithm (SHA-1 based) is disabled by default in recent OpenSSH. The RSA key itself stays valid — the problem is the legacy signature algorithm. If an old RSA key suddenly stops being accepted after a client or server upgrade, this is almost always the cause.

Generating an SSH key with ssh-keygen

ssh-keygen -t ed25519 -a 100 -C "alice@laptop-prod" -f ~/.ssh/id_ed25519_prod

Flag by flag:

  • -t ed25519 — key type.
  • -a 100 — KDF rounds used when encrypting the private key with your passphrase. More rounds make offline cracking of a stolen file more expensive; 100 keeps the unlock delay unnoticeable.
  • -C "alice@laptop-prod" — comment. It is appended to the public key and ends up in authorized_keys. Six months later this comment is how you decide whether a line can be deleted. Write the human and the machine, not "my key".
  • -f ~/.ssh/id_ed25519_prod — path. Distinct filenames per environment beat one id_ed25519 for everything.

Passphrase: yes or no

Yes. The passphrase encrypts the private key at rest. Without it, a stolen laptop, a forgotten home directory backup, or access to a synced folder hands an attacker working logins to every server you touch.

The "typing it every time is annoying" objection is solved by ssh-agent — the passphrase is entered once per session. A passphrase-less key is acceptable only for machine workflows (CI, deploys, backups), and such a key must be separate and tightly restricted.

You can add or change a passphrase without reissuing the key:

ssh-keygen -p -a 100 -f ~/.ssh/id_ed25519_prod

Check the fingerprint to be sure you are looking at the right file:

ssh-keygen -lf ~/.ssh/id_ed25519_prod.pub
# 256 SHA256:kx3Y... alice@laptop-prod (ED25519)

Creating an SSH key on Windows

Windows 10 and 11 ship a built-in OpenSSH client — no extra software required. The same ssh-keygen -t ed25519 -C "..." works in PowerShell, and keys land in C:\Users\Name\.ssh. The agent runs as a Windows service:

Get-Service ssh-agent | Set-Service -StartupType Automatic
Start-Service ssh-agent
ssh-add $env:USERPROFILE\.ssh\id_ed25519

PuTTY is a separate world. It does not read the OpenSSH format and uses its own .ppk. Keys are created in PuTTYgen and the agent is called Pageant. An existing OpenSSH key can be imported into PuTTYgen and saved as .ppk; the reverse conversion is available through the OpenSSH export option. Either way, what goes onto the server is the single-line public form ssh-ed25519 AAAA... comment — not the entire PuTTYgen text block with its headers.

How to add an SSH key to a server

The easy path is ssh-copy-id. It creates the directory, appends the key and fixes permissions:

ssh-copy-id -i ~/.ssh/id_ed25519_prod.pub -p 22 deploy@203.0.113.10

Always pass the .pub file explicitly. Without -i the tool pulls keys from the agent, and a key you did not intend may end up on the server.

If ssh-copy-id is unavailable (typical on plain macOS and on Windows), one command does the same job:

cat ~/.ssh/id_ed25519_prod.pub | ssh deploy@203.0.113.10 \
  "umask 077; mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"

umask 077 matters: it guarantees the directory and file are created with correct permissions immediately instead of relying on a follow-up chmod. Note the >> — a single > wipes every existing key, including your colleagues'.

Manual installation and verification

If the only way in is the provider's web console, log in as the right user and add the key by hand:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys      # paste one complete line
chmod 600 ~/.ssh/authorized_keys
chown -R "$USER:$USER" ~/.ssh

A public key is exactly one line. A line break in the middle breaks it: the server sees two garbage entries instead of one working key. Verify after pasting:

ssh-keygen -lf ~/.ssh/authorized_keys

The command prints one line per valid key. Fewer lines than keys you added means a wrapped paste.

The key belongs to the user you will log in as. /root/.ssh/authorized_keys and /home/deploy/.ssh/authorized_keys are different files. A classic mistake is installing the key into root's home under sudo and then wondering why ssh deploy@host still asks for a password.

File permissions: the number one cause of Permission denied (publickey)

SSH runs with StrictModes yes by default: if permissions on the directory or files are too loose, the server silently ignores the key and behaves as if it does not exist. The client only ever sees Permission denied (publickey). This accounts for roughly half of all "my key does not work" cases.

PathModeOwnerWhat breaks
/home/user750 or 700 (not group/world writable)userThe whole .ssh directory is rejected
~/.ssh700userKeys are not read at all
~/.ssh/authorized_keys600userKey silently ignored
~/.ssh/id_ed25519 (client)600userUNPROTECTED PRIVATE KEY FILE, client refuses the key
~/.ssh/config (client)600userBad owner or permissions

One-shot fix on the server:

chmod go-w ~
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
chown -R "$(id -un):$(id -gn)" ~/.ssh
restorecon -Rv ~/.ssh 2>/dev/null || true

The last line matters on SELinux systems (RHEL, Rocky, AlmaLinux, Fedora): a .ssh directory restored from an archive or copied out of /tmp carries the wrong security context, and sshd cannot read it even with formally correct modes. On Debian and Ubuntu the command is simply absent and is skipped safely.

Using ~/.ssh/config and ssh-agent

Past three servers, long commands with -i, -p and full usernames become unbearable. The fix is a client config with host aliases.

# ~/.ssh/config

Host prod-web
    HostName 203.0.113.10
    User deploy
    Port 2222
    IdentityFile ~/.ssh/id_ed25519_prod
    IdentitiesOnly yes

Host db-internal
    HostName 10.0.5.20
    User admin
    IdentityFile ~/.ssh/id_ed25519_prod
    ProxyJump prod-web

Host *
    ServerAliveInterval 30
    ServerAliveCountMax 3
    HashKnownHosts yes

Now ssh prod-web does everything the long command did, and ssh db-internal transparently routes through the bastion with no manual tunnelling.

The critical setting is IdentitiesOnly yes. Without it the client offers every key in the agent, one after another. With six keys loaded and MaxAuthTries 6 on the server, attempts run out before the right key is reached and you get Too many authentication failures.

ssh-agent: type the passphrase once

# Linux: start the agent if the session did not
eval "$(ssh-agent -s)"

ssh-add ~/.ssh/id_ed25519_prod     # asks for the passphrase once
ssh-add -l                          # which keys are loaded
ssh-add -D                          # unload everything

A good habit is bounding key lifetime in the agent: ssh-add -t 4h ~/.ssh/id_ed25519_prod. When the timer expires the key unloads itself, so a forgotten unlocked session stops being permanent access.

On macOS the agent integrates with Keychain: ssh-add --apple-use-keychain ~/.ssh/id_ed25519_prod stores the passphrase, and UseKeychain yes in ~/.ssh/config makes the client retrieve it automatically.

Do not put ForwardAgent yes under Host *. Agent forwarding lets root on the intermediate host use your keys for as long as the session is open. If you genuinely need it, enable it for one specific bastion — or better, replace it with ProxyJump, which does not expose the agent at all.
Diagram of a client ssh config with host aliases, IdentityFile entries and a ProxyJump hop to an internal server
Host aliases and ProxyJump remove long commands and manual tunnels.

Disabling password login, root login and changing the SSH port

Keys alone close nothing: while PasswordAuthentication yes is in place, bots keep guessing. The benefit appears only once password login is off.

Edit /etc/ssh/sshd_config:

PubkeyAuthentication yes
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
PermitEmptyPasswords no

MaxAuthTries 3
LoginGraceTime 20
AllowUsers deploy admin

What matters here:

  • KbdInteractiveAuthentication no is mandatory alongside PasswordAuthentication no. Otherwise, on PAM-based systems, passwords remain reachable through the keyboard-interactive method and you get a false sense of a closed door.
  • PermitRootLogin prohibit-password keeps root reachable by key (some automation needs it) while blocking passwords. If root is not needed at all, use no.
  • AllowUsers is an allow-list. Even a leaked service key cannot be used to log in as somebody else.

The Include and sshd_config.d trap

Modern distributions start sshd_config with Include /etc/ssh/sshd_config.d/*.conf. For sshd the first match wins, so a value set in an included file overrides anything you write further down the main file. Cloud images frequently ship a drop-in that re-enables password authentication.

So check the effective configuration, not the file you edited:

sudo sshd -T | grep -Ei 'passwordauth|permitroot|pubkeyauth|kbdinteractive|^port'
sudo grep -r -n 'PasswordAuthentication' /etc/ssh/

sshd -T prints the resolved values with all includes applied. If it says passwordauthentication yes while your file says no, hunt down the overriding drop-in.

sshd -t and the second-session rule

A typo in sshd_config means the daemon will not come back after a restart. If SSH is the only way into the box, you have just lost it until you reach the provider console.

# 1. Syntax check BEFORE restarting
sudo sshd -t

# 2. Only if the check is silent — apply
sudo systemctl reload ssh || sudo systemctl reload sshd

# 3. In a SEPARATE terminal window — verify login
ssh -o PreferredAuthentications=publickey deploy@203.0.113.10
Keep your current SSH session open until a new one connects successfully. reload does not drop established connections — that session is your rollback path. Close both windows without testing and your only remaining options are the provider console or a rescue boot.

Changing the SSH port: what it buys and what it does not

Moving SSH off 22 is popular advice, and it helps with exactly one thing: mass scanners that only hit 22 stop flooding auth.log, which makes the log readable again.

What a port change does not buy: security. A full port sweep finds SSH in seconds, and the banner reveals the daemon version regardless of port number. If password login is still enabled, an obscure port will not save you. Treat it as log hygiene, not a control. For the wider picture on what should be exposed at all, see the write-up on open port security.

# /etc/ssh/sshd_config
Port 2222

Three things are routinely forgotten afterwards:

  • Open the port in the firewall before restarting: sudo ufw allow 2222/tcp or sudo firewall-cmd --permanent --add-port=2222/tcp && sudo firewall-cmd --reload.
  • On SELinux systems, label the port for SSH: sudo semanage port -a -t ssh_port_t -p tcp 2222. Without it the daemon cannot bind.
  • Check whether SSH is socket-activated. On recent Ubuntu the daemon starts through ssh.socket, and the Port directive in sshd_config is ignored — the port lives in the socket override.
# If ssh.socket is active, the port is set here
systemctl is-enabled ssh.socket
sudo systemctl edit ssh.socket

# in the editor:
# [Socket]
# ListenStream=
# ListenStream=2222

The empty ListenStream= line is required: it clears the inherited value of 22, otherwise the daemon listens on both ports.

SSH keys for Git: GitHub, GitLab and deploy keys

Git over SSH uses the same mechanics. A dedicated key for Git hosting is good practice: compromising it does not hand over your production servers.

ssh-keygen -t ed25519 -C "alice@github" -f ~/.ssh/id_ed25519_github
cat ~/.ssh/id_ed25519_github.pub      # paste this into your account settings

# verify ("successfully authenticated" is the expected reply — there is no shell)
ssh -T git@github.com
ssh -T git@gitlab.com

Pinning the key to the host in the config removes all guesswork:

Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_github
    IdentitiesOnly yes

Host gitlab-work
    HostName gitlab.com
    User git
    IdentityFile ~/.ssh/id_ed25519_work
    IdentitiesOnly yes

The second block solves the common "two accounts on one host" problem: clone as git clone git@gitlab-work:group/project.git and the client picks the right key.

Why a deploy key must be separate

A developer's personal key dropped onto a server for git pull grants that server access to every repository the human can reach. One compromised web server then becomes a full source-code breach.

The correct model: one key per server and repository, read-only, without a passphrase (automation cannot type one), with an explicit review date. On the server side, scope it directly in authorized_keys:

# a single line in ~/.ssh/authorized_keys
restrict,from="203.0.113.0/24",command="/usr/local/bin/deploy.sh" ssh-ed25519 AAAAC3Nza... ci@runner
  • restrict disables everything optional at once: port forwarding, agent forwarding, X11, PTY allocation.
  • from= pins the key to a source network.
  • command= forces a single command: whatever the client asks for, only this runs.
Diagram showing key separation: a personal admin key, a dedicated Git hosting key, and a restricted deploy key using restrict and command options
Different jobs, different keys. Compromising one does not cascade.

How to remove an SSH key and revoke access

Revocation is not "change the password". A key keeps working as long as its line physically sits in an authorized_keys file on at least one server.

# 1. Find every place the key comment or fingerprint appears
sudo grep -rn "alice@laptop" /home/*/.ssh/authorized_keys /root/.ssh/authorized_keys 2>/dev/null

# 2. Remove the line (back it up first)
sudo cp ~/.ssh/authorized_keys ~/.ssh/authorized_keys.bak
sudo sed -i '/alice@laptop/d' /home/deploy/.ssh/authorized_keys

# 3. Confirm the remaining list is what you expect
ssh-keygen -lf /home/deploy/.ssh/authorized_keys

Check every location, not just the obvious one:

  • home directories of all users, including service accounts and root;
  • the legacy ~/.ssh/authorized_keys2, if it survived from older systems;
  • non-standard paths: sudo sshd -T | grep authorizedkeysfile — some setups store keys under /etc/ssh/authorized_keys/%u;
  • keys in CI, provider panels, Git hosting (both account keys and deploy keys), golden images and VM templates;
  • live sessions: deleting a line does not terminate an established connection — review who and use pkill -u username sshd.

Across a fleet, a central denylist is easier. The RevokedKeys directive in sshd_config points at a file of revoked public keys, which are rejected even if a stale line survives somewhere in an authorized_keys.

Separately, "remove an SSH key" sometimes means the server entry in the client's known_hosts:

ssh-keygen -R 203.0.113.10
ssh-keygen -R "[203.0.113.10]:2222"     # for a non-standard port

Debugging with ssh -vvv: the common errors

The universal first step is the verbose client log:

ssh -vvv -o PreferredAuthentications=publickey -i ~/.ssh/id_ed25519_prod deploy@203.0.113.10

Look for Offering public key: and what follows it. If the server keeps replying Authentications that can continue: publickey, the key was offered and rejected — the problem is server-side. If the client never offers the file at all, the problem is local: wrong path, wrong permissions, key not in the agent.

Server side (requires a second way in):

sudo journalctl -u ssh -f
sudo tail -f /var/log/auth.log        # Debian/Ubuntu
sudo tail -f /var/log/secure          # RHEL family

Permission denied (publickey)

The most common and least informative error. Work through it in order:

  1. Permissions: ~ not group-writable, ~/.ssh at 700, authorized_keys at 600, owner matching the login user.
  2. Wrong user: the key sits in deploy's home while you connect as root.
  3. Wrong key: compare ssh-keygen -lf key.pub with ssh-keygen -lf authorized_keys — fingerprints must match.
  4. Broken line: a paste that wrapped in the middle of the key.
  5. User filtered out by AllowUsers/AllowGroups, or the account is locked (passwd -S deploy reporting L).
  6. Wrong SELinux context on .ssh.

Bad owner or permissions on /home/user/.ssh/config

The client refuses to read a config writable by group or others. One command fixes it: chmod 600 ~/.ssh/config. The same logic applies to the directory itself.

WARNING: UNPROTECTED PRIVATE KEY FILE!

The private key is readable by someone other than the owner. Run chmod 600 ~/.ssh/id_ed25519. A frequent source is copying keys from a USB stick or a Windows partition where POSIX permissions are not preserved.

Host key verification failed

The server fingerprint no longer matches the one stored in known_hosts. Legitimate causes: OS reinstall, migration, restore from backup, IP change. Illegitimate cause: an active man-in-the-middle. Confirm the new fingerprint through a second channel (provider console, internal documentation) before clearing the old entry with ssh-keygen -R. Turning on StrictHostKeyChecking no just legalises server substitution.

Too many authentication failures

The agent offered more keys than MaxAuthTries permits. Fix it with IdentitiesOnly yes and an explicit IdentityFile per host.

no matching host key type found / ssh-rsa

An old SHA-1 RSA key meeting a modern OpenSSH. The right fix is reissuing the key as ed25519. The temporary workaround is allowing the algorithm for that one host with PubkeyAcceptedAlgorithms +ssh-rsa — which postpones the problem rather than solving it.

Connection refused / Connection timed out

Not an authentication issue at all. refused means the port is closed or the daemon is down: check systemctl status ssh. timed out means a firewall or cloud security group is dropping traffic. Both must be tested from outside, not from the machine itself.

Troubleshooting tree for SSH errors, from Permission denied publickey through permission, user and fingerprint checks down to server-side logs
Diagnosis starts with ssh -vvv and ends in the sshd logs.

How to verify your setup

After the edits, look at the server from the outside. Local ss -tlnp and systemctl status only prove the daemon is alive; they say nothing about what the internet sees.

  • Is the SSH port reachable? Scan the host with the port scanner. If 22 (or your custom port) answers the whole world, the next question is whether it should. The stronger setup restricts SSH by source IP at the firewall or cloud security group. How to read scan results is covered in the guide on checking open ports.
  • General server hygiene. Run the domain through the security check: headers, TLS, exposed configuration artefacts. SSH is one door; locking it does nothing about an open admin panel or a missing HSTS header. The perimeter-wide checklist lives in the article on web server hardening.
  • Effective config check. sudo sshd -T | grep -Ei 'passwordauth|permitroot|kbdinteractive' is the only reliable source of truth about what actually applied.
  • An actual password attempt. ssh -o PubkeyAuthentication=no -o PreferredAuthentications=password deploy@host should be refused without ever prompting. If you get a password prompt, password login is still on.

For admin keys there is a level above this: a hardware token (ssh-keygen -t ed25519-sk) requires a physical touch on every connection, so a stolen key file is useless on its own. The reasoning mirrors modern web authentication — see the breakdowns of two-factor authentication and passkeys versus classic 2FA.

Frequently asked questions

Can I use one SSH key for all servers?

Technically yes, and for a personal admin key it is reasonable: one key, revoked in one place. Trouble starts when the same key also ends up in CI, in a container and on a colleague's laptop. The rule is one key per identity — you, a specific CI runner, a specific service. Not one key for everyone.

What do I do if a private key leaks?

Immediately remove the matching public key from authorized_keys on every server and from every Git hosting account, terminate that user's active sessions, then generate a new pair and roll it out. Changing the passphrase on a leaked key is pointless: the attacker already has the file and can crack the passphrase offline at leisure.

Do I still need fail2ban if passwords are disabled?

Its value drops but does not vanish. Key guessing is futile, yet fail2ban trims log noise, reduces load from mass scanners and catches probing of other services. As the sole protection for SSH it is unnecessary — disabled passwords are stronger.

Why does the key work for one user but not another?

Almost always ownership or permissions. A .ssh directory copied from another user with cp -r under sudo keeps the old owner, and sshd rejects it. Run ls -ld ~ ~/.ssh ~/.ssh/authorized_keys — the owner must match the login user.

Must I disable root SSH login entirely?

At minimum, disable password login for root (PermitRootLogin prohibit-password). Blocking it completely and working through a normal user with sudo is better: you get personalised audit trails showing who ran a command instead of an anonymous root.

My key stopped working after a server rebuild — why?

A rebuild wipes authorized_keys along with the home directory, and the server gets a new host key. The first is fixed by reinstalling the key, the second by ssh-keygen -R plus confirming the new fingerprint through the provider console.

Checklist

  • Key generated as ed25519, with a meaningful comment and a passphrase.
  • The private key was never copied anywhere: no chat, no repository, no container image.
  • Public key installed for the correct user, as a single line, fingerprints matching.
  • Permissions set: home directory not group-writable, ~/.ssh at 700, authorized_keys at 600, correct owner.
  • Client ~/.ssh/config and private keys at 600.
  • Key login verified in a separate session before touching the server config.
  • PasswordAuthentication no and KbdInteractiveAuthentication no confirmed through sshd -T, not just written to a file.
  • sshd_config.d checked for overriding drop-ins.
  • sudo sshd -t run before restarting, with a second session held open.
  • If the port changed: opened in the firewall, labelled for SELinux, ssh.socket accounted for.
  • Separate keys issued for Git and deployment; the deploy key scoped with restrict and command=.
  • A revocation procedure exists: where to look, who executes it, how the result is verified.
  • Port verified from outside with the port scanner, the perimeter with the security check.

Check your website right now

Check your site's security →
More articles: Security
Security
How to Check a Website for Malware: 4 Layers of Detection and a Cleanup Plan
01.04.2026 · 959 views
Security
Web Server Security Hardening Checklist: Nginx and Apache
16.03.2026 · 457 views
Security
HSTS and Preload List: Complete Implementation Guide
16.03.2026 · 365 views
Security
How to Check a Website for Fraud: 12 Signs of a Phishing Site
18.07.2026 · 303 views