How to Generate an SSH Key (2026): Guide with 11 Real Failure Tests

How to Generate an SSH Key (2026): Guide with 11 Real Failure Tests

To generate an SSH key takes about a minute: type ssh-keygen -t ed25519, set a passphrase twice, done. You end up with two files in ~/.ssh/, and the public half has to go onto the server. Every tutorial covers that much.

What most tutorials skip is the part after that. Why does the login fail even though the key is correct? Which file permissions does the server reject, and which does it quietly accept? What happens if the key wraps onto two lines when you paste it? That is where the real time gets lost, because the client shows the same unhelpful message in almost every case: Permission denied (publickey).

So for this article we did more than retell the docs. On our production server (Ubuntu 24.04, OpenSSH 9.6p1) we started a separate test SSH daemon on a local port, created a throwaway user and deliberately broke things in eleven typical ways. For each failure you will see below what the client says and what the server log says. We also timed the key types against each other.

Three findings up front:

1. In our test, an Ed25519 key took 9 milliseconds on average to generate. RSA 4096 took 1,322 milliseconds, roughly 150 times longer. The Ed25519 public key is 86 bytes, the RSA 4096 public key 730 bytes.

2. In 10 of 11 failure cases, the client only printed Permission denied (publickey). The actual reason was only in the server log. If you only look at the client, you are guessing.

3. Two mistakes that many guides warn about did not break the login for us: Windows line endings (CRLF) in authorized_keys and a group-writable .ssh directory. Why the second one is still a bad idea is explained below.

If you first want to understand what SSH is and why keys are safer than passwords, start with our primer What is SSH?. This article is about the craft.

A glowing key splits into two halves: one stays at the laptop, the other travels to the lock on the server

Generate an SSH key in 60 seconds: the short version

For anyone who only needs the commands. This works the same on Linux, macOS and Windows 10/11 (PowerShell):

# 1. Generate the key pair
ssh-keygen -t ed25519 -C "work-laptop-2026"

# 2. Copy the public key to the server (Linux/macOS)
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server-ip

# 3. Test it
ssh user@server-ip

In step 1, ssh-keygen asks for a location (Enter accepts ~/.ssh/id_ed25519) and twice for a passphrase. Set one. The passphrase section explains why.

If step 3 works without the server asking for a password, you are done. If not, jump straight to The 11 failure tests.

What actually happens when you generate an SSH key

ssh-keygen creates a key pair: two files that belong together mathematically.

FileContentsWhere it goes
~/.ssh/id_ed25519private keystays on your machine, never leaves it
~/.ssh/id_ed25519.pubpublic keycan go anywhere: servers, GitHub, GitLab

The server only gets the public half. It goes into the ~/.ssh/authorized_keys file of the user you log in as. On login, the server sends a challenge that can only be answered by whoever holds the matching private key. The private key itself is never transmitted. That is the core idea: even if someone records the connection or takes over the server, they do not get your private key.

This is what an Ed25519 public key looks like, exactly one line with three parts:

ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...  work-laptop-2026
│           │                              └─ comment (free text)
│           └─ the actual key (Base64)
└─ type

The private key starts with -----BEGIN OPENSSH PRIVATE KEY-----. A detail we noticed while testing: for a key with a passphrase, the second line already contains, Base64-encoded, aes256-ctr and bcrypt. That is how OpenSSH encrypts the file. Without a passphrase it says none.

Two matching puzzle pieces: one kept in a safe, the other copied onto several servers

Which key type? Ed25519 vs. RSA, measured

The recommendation is clear: Ed25519. So it is not just a claim, we generated five keys per type on our server and timed them:

TypeGeneration (avg of 5)Public keyPrivate key
Ed255199 ms86 bytes387 bytes
ECDSA 2566 ms166 bytes492 bytes
RSA 3072427 ms558 bytes2,590 bytes
RSA 40961,322 ms730 bytes3,369 bytes

Measured on 2026-10-01, OpenSSH 9.6p1, no passphrase, wall clock time including process start.

Generation time does not matter to a human; anyone can wait a second. Two other points matter more:

Length. An Ed25519 key fits on a short line. That sounds cosmetic but is practical: when you copy it through a terminal, a web panel or a note, it is less likely to wrap. And a wrapped key, as test 9 shows, is a hard failure.

Robustness. Ed25519 is designed so that typical implementation mistakes do less damage. ECDSA is sensitive to bad randomness during signing. RSA is not broken, but it needs at least 3072 bits, and many old guides still recommend 2048.

When RSA after all? Only if a target system cannot do Ed25519. Today that mostly means very old network gear or appliances with OpenSSH older than 6.5 (from 2014). Then use ssh-keygen -t rsa -b 4096.

DSA is dead. OpenSSH removed support in version 9.8 in 2024.

What your server accepts can be queried directly. The list is long, Ed25519 is near the front:

sshd -T | grep pubkeyacceptedalgorithms

And in practice: all 7 successful key logins in our current journal used Ed25519. Not a single RSA login.

Three keys of different sizes side by side, each with a stopwatch

Step 1: Generate the SSH key with ssh-keygen

The full command we recommend:

ssh-keygen -t ed25519 -a 100 -C "work-laptop-2026" -f ~/.ssh/id_ed25519

What the options mean:

  • -t ed25519 picks the type.
  • -a 100 sets how many rounds the passphrase goes through the key derivation (bcrypt). More rounds make guessing a stolen file more expensive. The default is 16.
  • -C "..." sets the comment. Without it, ssh-keygen uses user@hostname.
  • -f sets the file name. Without it, the program asks.

The output looks roughly like this:

Generating public/private ed25519 key pair.
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:CHAHGMoAvYw9cfsCl/+c7F1ubJ5FIG7YMa1WPcZ2MvE work-laptop-2026
The key's randomart image is:
+--[ED25519 256]--+
|+.ooo..       .  |
|o.+o..     . o o |
...

The fingerprint (SHA256:...) is the short, unique identifier of the key. It is how you will recognise the key in the server log later. The “randomart” picture below it is a visual version of the same thing, meant to help tell two keys apart by eye. In practice almost nobody uses it.

What does -a 100 cost when unlocking?

More rounds protect better, but you pay on every unlock. We measured how long it takes to unlock an Ed25519 key at different round counts:

-a (rounds)Unlock takes
16 (default)112 ms
64426 ms
100667 ms
2001,338 ms

The curve is linear: twice the rounds, twice the time. For an attacker trying to brute-force a stolen file, the same ratio applies per guess. With -a 100, each guess costs about six times as much as with the default. You notice a good half second, and with ssh-agent (see below) only once per session.

Multiple keys? Give them names

If you want separate keys for separate purposes (work, personal, GitHub), use -f:

ssh-keygen -t ed25519 -C "github-personal" -f ~/.ssh/id_ed25519_github

How SSH then picks the right key automatically is covered in the ~/.ssh/config section.

Step 2: Get the public key onto the server

Linux and macOS: ssh-copy-id

The most convenient and safest way:

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server-ip

ssh-copy-id logs in once with your password, creates ~/.ssh if it is missing, appends the key to authorized_keys and sets the permissions correctly. That last part matters, as the failure tests below show.

Windows: without ssh-copy-id

Windows 10 and 11 ship the OpenSSH client, so ssh and ssh-keygen are there. ssh-copy-id is not. In PowerShell:

type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh user@server-ip "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"

On Windows the keys live in C:\Users\YOURNAME\.ssh\.

By hand, when there is no other way

Some hosts only give you a web panel or a browser console. Then:

mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys     # paste the key as ONE line
chmod 600 ~/.ssh/authorized_keys

Important: the key must be on exactly one line. Many web consoles and editors wrap long lines when pasting. We reproduced exactly that in test 9.

Cloud servers: best done when ordering

Hetzner, DigitalOcean, AWS and most other providers let you add a public key when creating the server. It then lands in /root/.ssh/authorized_keys, and password login is often disabled from the start. That is the cleanest start. What to do next is in our guide Linux server setup.

Step 3: Test before you switch anything off

ssh -i ~/.ssh/id_ed25519 user@server-ip

If that works without asking for the server’s password, the key works. Being asked for your key’s passphrase is normal; that is your local lock.

If you want to see what is happening, add -v. The two lines that matter:

debug1: Offering public key: /home/you/.ssh/id_ed25519 ED25519 SHA256:...
debug1: Server accepts key: /home/you/.ssh/id_ed25519 ED25519 SHA256:...

If you see Offering but never Server accepts key, the server rejected the key. It deliberately does not tell the client why. The reason is only in the server log, hence the next section.

Tom Lawrence (Lawrence Systems) walks through the full path from generating Ed25519 keys to disabling password login.

The 11 failure tests: what the server actually rejects

The setup

We did not want to touch our server’s real SSH configuration. So the test worked like this:

  • a second sshd bound only to 127.0.0.1, port 2299, with its own config and its own host key
  • PasswordAuthentication no, StrictModes yes (the default), LogLevel VERBOSE
  • a throwaway user skmtest, deleted along with its home directory after the test
  • exactly one thing broken per test, then repaired

That way each failure shows up on its own without two of them overlapping. The results:

#What we brokeClient saysServer log saysLogin
0user freshly created with useraddPermission deniedUser skmtest not allowed because account is locked❌
1nothing (baseline)–Accepted publickey ... ED25519✅
2wrong key (RSA instead of Ed25519)Permission deniedFailed publickey ... RSA❌
3private key with permissions 644WARNING: UNPROTECTED PRIVATE KEY FILE!nothing, client aborts first❌
4authorized_keys group-writable (664)––✅ ⚠️
5home directory 777Permission deniedbad ownership or modes for directory /home/skmtest❌
6.ssh directory 755––✅
7authorized_keys owned by rootPermission deniedFailed publickey❌
8authorized_keys with Windows line endings (CRLF)––✅
9key wrapped onto two lines when pastingPermission deniedFailed publickey❌
10key with passphrase, no agent, non-interactivePermission denied(client offers nothing)❌
11from="10.0.0.0/8" in front of the keyPermission deniedRefused by key options❌

The “Client says” column is why SSH debugging is so tedious. Apart from test 3, the client always sees the same thing.

Test 0: The failure we did not plan

The first run failed before we had broken anything. The user had just been created with useradd, the key was in place, the permissions were right, and still: Permission denied. The log said:

User skmtest not allowed because account is locked

useradd creates new accounts with a locked password (! in /etc/shadow). Without PAM, OpenSSH treats that as a locked account and refuses keys too. The fix is to set the password to “none possible” instead of “locked”:

usermod -p '*' username

Honest caveat: our test sshd ran without PAM. A normal Ubuntu or Debian server has UsePAM yes in its sshd_config, and there this case usually does not occur. On systems without PAM (some containers, Alpine, BSD), it can easily cost you half an hour. We would not have found it without looking at the log either.

Test 3: The only failure the client reports

If your private key sits on disk with permissions that are too open, the client refuses to use it and says so loudly:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@

Fix: chmod 600 ~/.ssh/id_ed25519. This typically happens when you copy keys between machines via a USB stick, a shared drive or a ZIP archive.

Tests 4 and 6: When group-writable still gets through

Many guides say authorized_keys must be 600 and .ssh must be 700, otherwise nothing works. Our test only half confirmed that. With 644 on authorized_keys and 755 on .ssh, login worked fine, because others can only read, not write. Reading a public key is not a problem.

Test 4 was more surprising: even with 664, i.e. group-writable, the login worked. The reason is a Debian and Ubuntu quirk: OpenSSH there tolerates group write permission if the group contains only the user themselves. That is the default with useradd; every user gets their own group.

To check this, we added a second user to skmtest’s group and repeated the test. Result:

Authentication refused: bad ownership or modes for file /home/skmtest/.ssh/authorized_keys

So the server refuses exactly when someone else really could change the file. That makes sense, but it is a trap: a setup that works today breaks tomorrow when someone adds a colleague to the group. That is why we stick to 700 and 600. Not because anything else breaks immediately, but so it does not break by surprise later.

Test 5: The home directory counts too

Many people miss this one: OpenSSH checks not only .ssh and authorized_keys but the entire path leading to them. If the home directory is writable by everyone (777), someone else could rename .ssh and replace it with their own. So the server refuses key login entirely:

Authentication refused: bad ownership or modes for directory /home/skmtest

In practice this happens after an overly generous chmod -R 777 that someone used to “fix” a completely different permission problem. Fix: chmod 750 ~ or chmod 755 ~.

Test 7: The file belongs to the wrong user

If you run nano /home/user/.ssh/authorized_keys as root and create the file fresh, root owns it. With permissions 600, the actual user cannot even read it. Result: Failed publickey. Fix:

chown -R user:user /home/user/.ssh

Test 8: Windows line endings are harmless

We expected CRLF line endings (the \r from Windows) to make the key unusable. In OpenSSH 9.6 they do not; the login worked. Good to know if you edited the file on Windows. With older versions or other SSH servers (such as Dropbear on routers) we would not rely on it.

Test 9: A line break kills the key

A key that got split across two lines when pasting is read by the server as two broken entries. Result: Failed publickey, without any further hint. You can check this quickly:

ssh-keygen -lf ~/.ssh/authorized_keys

This prints a fingerprint for every valid line. If one is missing or it reports an error, the file is broken.

Test 10: Passphrase without an agent in scripts

A key with a passphrase does not work in scripts, cron jobs or CI when nobody can type the passphrase (BatchMode=yes). The client silently skips it. For automation there are two clean options: a dedicated key without a passphrase that is tightly restricted on the server (see test 11), or a running ssh-agent.

Test 11: Restricting keys

authorized_keys lets you put conditions in front of each key. We tested two of them:

from="10.0.0.0/8" ssh-ed25519 AAAA... backup-server
from="127.0.0.1",command="echo forced" ssh-ed25519 AAAA... test

With from= from the wrong network, the server refused: Refused by key options. With command=, instead of our command rm -rf / (in a throwaway account, mind you) only echo forced ran. The server ignores what the client wants to run and executes its fixed command.

That is the right tool for keys without a passphrase, for example for backups: the key only works from one IP and can only run one command. If it is stolen, the damage is limited. Other options include no-port-forwarding, no-pty, or simply restrict, which turns everything off.

A heavy door that will not open because one of three padlocks on the chain is broken

Debugging in one command

Because the client reveals so little, this is the most important debugging command. On the server, while you log in from a second window:

journalctl -u ssh -f

On older systems the service is called sshd, or the messages go to /var/log/auth.log. Lines containing Authentication refused, bad ownership, not allowed or Refused by key options tell you exactly what is going on.

Passphrase and ssh-agent: security without the typing

A private key without a passphrase is a file that works the moment someone copies it: from a backup, a stolen laptop, an accidentally pushed repository. With a passphrase it is encrypted, and an attacker has to crack it first. With -a 100, each guess costs about two thirds of a second of compute on normal hardware.

So you do not have to type the passphrase on every connection, there is ssh-agent. It keeps the unlocked key in memory until you log out:

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
ssh-add -l     # lists the keys the agent currently holds

Most Linux desktops (GNOME, KDE) and macOS already run an agent automatically. On macOS the keychain can store the passphrase permanently:

ssh-add --apple-use-keychain ~/.ssh/id_ed25519

On Windows the agent is a service that is disabled by default. Once, in an administrator PowerShell:

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

A key in a transparent vault, next to a helper holding the unlocked key in a glowing crystal

Changing passphrase and comment later

Both work without generating a new key:

ssh-keygen -p -f ~/.ssh/id_ed25519                          # change or add passphrase
ssh-keygen -c -C "new-comment" -f ~/.ssh/id_ed25519         # change comment

We verified that the fingerprint does not change. Before and after both changes it was the same SHA256:CHAHGMo.... The server notices nothing, and you do not have to redistribute anything.

That also implies something we showed in detail in What is SSH?: the comment is free text and proves nothing. What a key is called only tells you what someone was thinking when they created it. Who actually uses it is in the log, via the fingerprint.

Forgot the passphrase? Then the key is lost. There is no way back; that is the whole point. Generate a new key and remove the old entry from authorized_keys on every server.

~/.ssh/config: never type -i again

As soon as you have more than one key or more than one server, a ~/.ssh/config pays off:

Host myserver
    HostName 203.0.113.10
    User deploy
    IdentityFile ~/.ssh/id_ed25519
    IdentitiesOnly yes

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

After that, ssh myserver is enough. IdentitiesOnly yes matters more than it looks: without it, the client tries every key in the agent one after another. If you have five or six, the server gives up after MaxAuthTries (6 on our server), and you get Too many authentication failures even though the right key was among them.

SSH key for GitHub and GitLab

The process is the same, except the public key goes into a web form instead of a file:

  1. Show your public key: cat ~/.ssh/id_ed25519.pub
  2. Copy the entire line
  3. GitHub: Settings → SSH and GPG keys → New SSH key. GitLab: Preferences → SSH Keys
  4. Test:
ssh -T git@github.com
# Hi username! You've successfully authenticated, but GitHub does not provide shell access.

For GitHub we recommend a dedicated key (see above, -f ~/.ssh/id_ed25519_github). If it gets lost, you only have one place to clean up.

A short walkthrough from ACCESS for CI showing ssh-keygen with Ed25519 on macOS, Linux and Windows.

Hardware keys: ed25519-sk

If you have a FIDO2 security key such as a YubiKey, you can bind the private key to it:

ssh-keygen -t ed25519-sk -C "yubikey"

Then the file on disk is only a handle that is useless without the physical key. Every login requires touching the key. This needs OpenSSH 8.2 or newer on both ends. For admin access to important servers, it is the strongest protection SSH offers without additional infrastructure. We did not measure this ourselves for this article, because there is no security key plugged into the server.

Afterwards: disable password login

A key only adds security if the password no longer works as a back door. Only after confirming in a second session that the key works, in /etc/ssh/sshd_config or a file under /etc/ssh/sshd_config.d/:

PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password

Then check and reload:

sshd -t && systemctl reload ssh
sshd -T | grep -iE "passwordauth|kbdinteractive"

The last command asks the running daemon, not the file. This is what it looks like on our server:

passwordauthentication no
kbdinteractiveauthentication no

What that means in practice: our current journal (it currently only goes back to 2026-09-29, a little over 42 hours) contains 19,497 login attempts with non-existent usernames and zero Failed password entries. The bots keep trying passwords, but the server no longer even offers a password prompt. There is nothing to guess. How we keep the remaining noise down is covered in fail2ban setup and UFW firewall setup.

What we did not measure

So nobody reads more into the numbers than is there:

  • The generation times come from one server and five runs each. On a laptop or Raspberry Pi the absolute values differ. The ratio between Ed25519 and RSA stays similar.
  • The failure tests ran against Ubuntu’s OpenSSH 9.6p1, without PAM. Tests 0 and 4 depend on the distribution and on PAM. Other versions may behave differently.
  • The 19,497 attempts come from a window of about 42 hours, because our journal does not reach further back. For a longer analysis see our fail2ban article with 31 days of data.
  • We did not test hardware keys (ed25519-sk) or SSH certificates ourselves.

Frequently asked questions

How do I generate an SSH key?

Run ssh-keygen -t ed25519 -C "description" in a terminal. This works on Linux, macOS and in Windows 10/11 PowerShell. Confirm the location with Enter and set a passphrase. You then have the private key id_ed25519 and the public key id_ed25519.pub in ~/.ssh/.

Where are SSH keys stored?

On Linux and macOS in ~/.ssh/ (i.e. /home/user/.ssh/ or /Users/user/.ssh/), on Windows in C:\Users\USER\.ssh\. On the server, the allowed public keys are in the ~/.ssh/authorized_keys file of the respective user.

How do I show my public SSH key?

cat ~/.ssh/id_ed25519.pub (Windows: type $env:USERPROFILE\.ssh\id_ed25519.pub). The fingerprint is shown by ssh-keygen -lf ~/.ssh/id_ed25519.pub. Always copy the .pub file, never the one without an extension.

Ed25519 or RSA, which should I use?

Ed25519. In our test it generated about 150 times faster than RSA 4096, and the public key is 86 instead of 730 bytes. Use RSA (at least 3072 bits) only if a very old target system does not support Ed25519.

How do I copy the SSH key to the server?

On Linux and macOS with ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server. On Windows with the PowerShell one-liner from this article. For cloud servers, ideally add it in the provider’s panel when ordering.

Why do I get “Permission denied (publickey)” even with a key?

Because the server rejected the key and does not tell the client why. The most common causes in our tests: wrong ownership or overly open permissions on the home directory, a wrapped key in authorized_keys, a locked account, or simply the wrong key. journalctl -u ssh -f on the server shows the reason.

Do I need a passphrase?

Yes, for every key a human logs in with. Without a passphrase, a copied file is all it takes to get in. For automation without a passphrase, restrict the key on the server with from=, command= or restrict.

Can I change the passphrase later?

Yes, with ssh-keygen -p -f ~/.ssh/id_ed25519. The fingerprint stays the same, so nothing needs to change on the servers. A forgotten passphrase cannot be recovered.

Can I use one SSH key for multiple servers?

Technically yes; the public key can live on any number of servers. A better rule is one key per device (laptop, desktop, phone) rather than per server. If a device is lost, you remove exactly that one key everywhere.

How do I delete or revoke an SSH key?

Remove the corresponding line from ~/.ssh/authorized_keys on the server. Before that, list the fingerprints with ssh-keygen -lf ~/.ssh/authorized_keys and check the log to see whether the key is still in use. A rarely used key may be tied to a backup or a cron job.

Does the same SSH key work for GitHub?

Yes. Paste the contents of the .pub file under Settings → SSH and GPG keys and test with ssh -T git@github.com. We still recommend a dedicated key for GitHub.

Conclusion

To generate an SSH key is one command: ssh-keygen -t ed25519. Choosing the type is no longer a matter of taste. Ed25519 is shorter, faster and more robust, and every single key login on our server used it.

The time gets lost elsewhere: when it does not work. In ten of eleven failure cases the client told us nothing beyond Permission denied (publickey). The answer was in the server log every time, sometimes as bad ownership, sometimes as account is locked, sometimes as Refused by key options. If you remember one thing from this article, make it this: when keys fail, do not guess on the client; open journalctl -u ssh -f on the server.

And then, only after a successful test in a second session, disable password login. After that, the 19,497 bots in our journal have nothing left to guess.