Published: · Updated: · by kodewright
How to Generate an SSH Key Safely (2026 Guide)
Generate an SSH key with ssh-keygen: ed25519 vs RSA, what the passphrase and KDF rounds buy you if the key file leaks, ssh-agent, and offboarding.

To generate an SSH key safely in 2026, run one command and type a real passphrase when it asks:
ssh-keygen -t ed25519 -a 64 -C "you@laptop-2026"
-t ed25519 picks the key type, -a 64 sets how many bcrypt rounds protect the private key file on disk, and -C is a label that ends up in the server's authorized_keys, so you can tell keys apart later. The command is the easy part. Whether a lost laptop or a leaked backup turns into access to your servers depends on the passphrase, on how you load the key into an agent, and on how fast you can remove a key from servers when it has to go.
What each ssh-keygen flag does
| Flag | Purpose | Notes |
|---|---|---|
-t ed25519 | Key type | The default since OpenSSH 9.5 (October 2023). I still write it out in docs and scripts so an older machine doesn't quietly create something else |
-a 64 | KDF rounds | How many bcrypt_pbkdf rounds turn your passphrase into the key that encrypts the file. Older releases defaulted to 16; OpenSSH 10.6 (October 2026) raised the default to 32 |
-C "you@laptop-2026" | Comment | Free text appended to the public key line. Name the person and the device, not just an email address |
-f ~/.ssh/id_ed25519_work | File name | Only needed if you keep separate keys per context |
A real run, trimmed (the randomart block is cut):
$ ssh-keygen -t ed25519 -a 64 -C "you@laptop-2026"
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:IikmcGblWuCYS6a6eqQ24RzSyqc9o36qkoPLnzdOxLE you@laptop-2026
Two files come out of it. id_ed25519 is the private key and never leaves the machine. id_ed25519.pub is the single line you paste into GitHub or copy to servers.
Ed25519 or RSA in 2026
For a new key, ed25519. The public key is 68 characters of base64, signing is fast, and the signature nonce is derived from the key and the message (RFC 8032) instead of drawn from a random generator. That last point matters more than the size: ECDSA needs a fresh secret random number for every signature, and a repeated one leaks the private key, which is how Sony's PS3 signing key was recovered in 2010.
| Key type | Public key (base64 chars) | Security level | Status in OpenSSH |
|---|---|---|---|
| ed25519 | 68 | ~128-bit | Default since 9.5 |
| RSA 3072 | 544 | 128-bit (NIST SP 800-57) | Default size when you ask for RSA |
| RSA 2048 | 372 | 112-bit | Still accepted, too small for new keys |
| ECDSA P-256 | 140 | 128-bit | Works; depends on a good nonce per signature |
| DSA | Removed entirely in OpenSSH 10.0 (April 2025) |
My own keys are RSA, and they still work everywhere I need them. People sometimes read "OpenSSH 8.8 disabled ssh-rsa" (September 2021) as "RSA keys are dead", but what 8.8 turned off was RSA signatures over SHA-1. The same RSA key signs with SHA-256 or SHA-512 instead, automatically. If you also have RSA keys, two checks are worth more than replacing them on principle:
ssh-keygen -lf ~/.ssh/id_rsa.pub # first number is the key size: want 3072+
head -2 ~/.ssh/id_rsa # which file format the private key is in
If the second command prints -----BEGIN RSA PRIVATE KEY----- followed by Proc-Type: 4,ENCRYPTED, the key was saved in OpenSSL's old PEM format, which ssh-keygen used by default until OpenSSH 7.8 (August 2018). That format derives the encryption key from your passphrase with a single MD5 pass and no work factor. ssh-keygen -p -a 64 -f ~/.ssh/id_rsa rewrites it as -----BEGIN OPENSSH PRIVATE KEY----- with bcrypt_pbkdf, and the public key doesn't change.
Two other options exist. With a FIDO2 token, ssh-keygen -t ed25519-sk (OpenSSH 8.2+) keeps the private key on the hardware, and the file in ~/.ssh is only a handle that's useless without the token and a touch; OpenSSH's own notes say token support for Ed25519 is less common than for ECDSA, so -t ecdsa-sk is the fallback. The hybrid post-quantum mldsa44-ed25519 type was enabled in OpenSSH 10.6, but servers on anything older won't accept it, so I wouldn't make it anyone's only key yet.
What the passphrase is worth when the key file leaks
The passphrase only matters in one scenario, and it's the common one: someone has a copy of id_ed25519. A stolen laptop, a home directory in a backup, a dotfiles repo that grabbed ~/.ssh, malware that reads files. From then on they guess offline, with no rate limit and no log on your side.
OpenSSH encrypts the private key with AES-256-CTR and gets the AES key from your passphrase through bcrypt_pbkdf. Each KDF round runs a bcrypt-style key setup with 64 expansion passes, roughly one bcrypt hash at cost 6, and the 48 bytes of AES key and IV need two output blocks. At 32 rounds that's about 64 cost-6 bcrypt hashes per guess. Scaling hashcat's measured RTX 4090 bcrypt speed (184,000 per second at cost 5) gives roughly 1,440 guesses per second, the same as bcrypt at cost 12. That is my estimate from the algorithm, not a published benchmark of this file format, so treat the table as order of magnitude.
H = W × log₂(7776) ≈ W × 12.9 bits W = random words from the EFF list
average time = 7776^W / 2 / guesses per second
| Passphrase | Entropy | Average time, one RTX 4090, 32 rounds |
|---|---|---|
| None | 0 bits | No guessing needed: the file is usable as-is |
| A word plus a year you picked | Low | Falls to a wordlist with rules; treat it as none |
| 2 random EFF words | 25.8 bits | ~6 hours |
| 3 random EFF words | 38.8 bits | ~5 years |
| 4 random EFF words | 51.7 bits | ~40,000 years |
| 5 random EFF words | 64.6 bits | ~310 million years |
| 8 random printable ASCII characters | 52.6 bits | ~73,000 years |
Adjust for your situation: a key written with the old 16-round default falls twice as fast, an 8-GPU rig divides every row by 8 (four words drop to about 5,000 years), and -a 64 doubles every row. The numbers only hold for words picked at random. A phrase you made up yourself belongs in the second row.
I'd treat four random words as the floor and use five for keys that reach production. Raising -a costs you the same factor in decryption time, but with an agent you pay that once per session, so going from 32 to 64 rounds is cheap.
Don't invent the words. The passphrase mode in my Password Generator picks them with crypto.getRandomValues() and shows the entropy in bits as you change the word count; it runs in your browser and sends nothing anywhere. That matters for the table above: a generator built on Math.random() has internal state that can be reconstructed from a handful of outputs, and then the "random" words aren't. To see how a passphrase you already use compares, put one with the same shape (same number and kind of words, not the real one) into the Password Strength Checker. The trade-offs between word-based and character-based secrets are in passphrase vs password.
ssh-agent: type the passphrase once per session
If the passphrase has to be typed on every connection, sooner or later someone strips it off the key. The agent keeps the decrypted key in memory and signs on your behalf:
eval "$(ssh-agent -s)"
ssh-add -t 8h ~/.ssh/id_ed25519 # forget the key after 8 hours
ssh-add -l # list what the agent holds
On macOS, ssh-add --apple-use-keychain ~/.ssh/id_ed25519 stores the passphrase in the Keychain, and AddKeysToAgent yes plus UseKeychain yes under Host * in ~/.ssh/config load it on first use.
Be careful with agent forwarding (ForwardAgent yes or ssh -A). While you're connected, anyone with root on the remote host can use your forwarded agent to sign as you on other servers. To reach machines behind a bastion, ssh -J bastion target (ProxyJump) gets you there without exposing the agent.
Put the public key on the server, then turn passwords off
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server
That appends your .pub line to ~/.ssh/authorized_keys on the server. At login nothing secret crosses the network: after key exchange, the client signs a blob that includes the session identifier with the private key, and the server checks that signature against the public key it has on file (RFC 4252, section 7). The signature is bound to that one session, so a recorded login can't be replayed.
client server
| key exchange -> session identifier H |
| "publickey" request + signature over H |
| (made with the private key) ------------> |
| | verify with the key in
| | ~/.ssh/authorized_keys
| <------------------------- success / fail |
Once key login works, disable passwords in /etc/ssh/sshd_config:
PasswordAuthentication no
KbdInteractiveAuthentication no
Check what sshd will actually use before reloading, because files in /etc/ssh/sshd_config.d/ can override the main file (Ubuntu cloud images ship a 50-cloud-init.conf that may set PasswordAuthentication yes):
sudo sshd -t && sudo sshd -T | grep -iE 'passwordauth|kbdinteractive'
Keep your current session open and test a fresh login from a second terminal before you close anything. That leaves you a way back in if the config is wrong.
Permissions and fingerprints
OpenSSH refuses a private key that other users can read, so set them once:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/id_ed25519 ~/.ssh/authorized_keys
chmod 644 ~/.ssh/id_ed25519.pub
ssh-keygen -lf ~/.ssh/id_ed25519.pub prints the fingerprint: 256 SHA256:IikmcG… you@laptop-2026 (ED25519). The one that protects you is the server's host key fingerprint, shown on your first connection to a new host. Compare it with the value from the provider's console or from ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub run on the server, instead of typing "yes" on reflex.
A fingerprint is the SHA-256 of the decoded key blob, base64-encoded with the trailing = dropped. If you want to see that for yourself, decode the middle field of a .pub line to a file with the Base64 decoder, hash the file in the Hash Generator with base64 output, and the result matches what ssh-keygen prints.
Rotation and offboarding happen on the server
A private key can't be revoked from the client side. It stops working only when every authorized_keys line that trusts it is gone. When I've had to deal with people leaving, the work was exactly that: going through the servers and updating who had access. A few habits make that job short:
- One key per person per device. A lost laptop then means deleting one line, and nobody else has to rotate anything.
- Comments that say who and which machine (
-C "anna@work-laptop-2026"), sogrep anna ~/.ssh/authorized_keysacross hosts finds every line. - Keys for CI and deploy scripts restricted on the server side, for example
from="203.0.113.10",restrict,command="/usr/local/bin/deploy"in front of the key, instead of a passphrase nobody can type at 3 a.m. - Past a handful of servers, SSH certificates: sign user keys with a CA key (
ssh-keygen -s ca_key -I anna -n deploy -V +30d id_ed25519.pub) and let them expire instead of hunting lines down.
Never commit a private key, and keep ~/.ssh out of dotfiles repos and synced folders. If one does leak, treat it like any other leaked credential: remove it from servers first, then generate the replacement.
If you do one thing after reading this, run head -1 on each private key in ~/.ssh. Anything without a passphrase, or still in the old PEM format, gets ssh-keygen -p -a 64 -f <file> today.
FAQ
What is the best command to generate an SSH key in 2026?
ssh-keygen -t ed25519 -a 64 -C "you@laptop-2026", then a passphrase of four or five random words. On OpenSSH 9.5 and later -t ed25519 is already the default, but writing it out costs nothing and protects you on older systems.
Should I use ed25519 or RSA for SSH keys?
Ed25519 for anything new. RSA keys of 3072 bits or more are still fine and keep working after OpenSSH 8.8, which only disabled SHA-1 signatures. Don't generate new RSA-2048 keys: 112-bit security is the weakest level NIST still accepts.
Does an SSH key need a passphrase?
For any key a person uses, yes. Without one, a copied file is a working login. Four random EFF words at the current default of 32 rounds come to roughly 40,000 years on average on one RTX 4090 by my estimate, while a phrase you invented falls to a wordlist. For CI keys, restrict what the key can do on the server instead.
How do I add a passphrase to an existing SSH key?
ssh-keygen -p -f ~/.ssh/id_ed25519. It asks for the old passphrase (just press Enter if there was none) and re-encrypts the same key, so nothing changes on your servers. Add -a 64 to raise the KDF rounds at the same time. Run on an old PEM-format key, it also converts the file to the OpenSSH format.
Where are SSH keys stored?
In ~/.ssh/: id_ed25519 (private, permissions 600) and id_ed25519.pub (public). Back up the private key only somewhere encrypted, such as a password manager's file attachment or an encrypted disk image. The public key needs no protection.