SSH keys explained: Ed25519 vs RSA, key formats, passphrases and converting between them
You copied a key to another machine and the far end said invalid format. A Java library wants PEM and ssh-keygen gave you a file beginning -----BEGIN OPENSSH PRIVATE KEY-----. A form offers Ed25519, RSA and ECDSA and you would like a reason to pick one. SSH keys are simple objects in an unreasonable number of wrappers, and most of the pain is wrapping. By the end of this guide you will be able to name any key file from its first line, choose a type deliberately, convert between the OpenSSH and PEM forms in either direction, and handle passphrases without locking a deploy job out.
How key authentication works
A key pair is two numbers that fit together. The private key stays on your machine; the public key is copied to every server you want to reach, one line each in ~/.ssh/authorized_keys. The server never receives the private key. In the publickey method of RFC 4252 the client signs a blob that includes the session identifier negotiated during key exchange, and the server verifies that signature with the public key it has on file. Because the session id is inside the signed data, a captured signature is worthless in any other session.
The public key is therefore genuinely public — pasting it into GitHub or a cloud console leaks nothing — and the private key file is the entire identity: whoever holds it, plus its passphrase if any, is you. The server proves itself the same way with a host key, which is what known_hosts and the SHA256:… prompt on first connection are about.
Ed25519, RSA or ECDSA?
| Type | Size and strength | Public line | OpenSSH support | Use it when |
|---|---|---|---|---|
| Ed25519 | 256-bit curve, about 128-bit security; 32-byte public key, 64-byte signatures | 80 characters: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI… | 6.5 (January 2014); ssh-keygen default since 9.5 | Always, unless something stops you |
| RSA | 2048 bits (~112-bit), 3072 (~128-bit, ssh-keygen default since 8.0), 4096 | 370–720 characters: ssh-rsa AAAAB3NzaC1yc2EAAAADAQAB… | Always; SHA-1 signatures (ssh-rsa) off by default since 8.8 | An appliance, old Git server or library without Ed25519 |
| ECDSA | NIST P-256 / P-384 / P-521; 128- to 256-bit | ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY… | 5.7 (2011) | A policy mandates NIST curves |
| DSA | 1024 bits only | ssh-dss … | Off by default in 7.0 (2015), compiled out in 9.8, removed in 10.0 | Never; replace any you find |
Ed25519 wins on more than size. Its signatures are deterministic (RFC 8032): the per-signature nonce is derived from the key and the message, so there is no random number to get wrong. ECDSA is not broken, but each ECDSA signature needs a fresh, unpredictable nonce, and a reused or biased one hands the private key to anyone holding two signatures — the 2010 PlayStation 3 signing-key recovery was exactly that mistake. Ed25519’s curve is also far easier to implement in constant time than the NIST curves.
RSA at 3072 bits and up is fine, and an existing RSA key needs no replacing. What changed is the signature: the original ssh-rsa signature uses SHA-1, and OpenSSH 8.8 (September 2021) stopped accepting it by default. The same key signs as rsa-sha2-256 or rsa-sha2-512 (RFC 8332, since OpenSSH 7.2), so modern servers are unaffected; appliances and old Git servers that only implement the SHA-1 form produce the no mutual signature supported error covered below.
The formats you will meet
The key material never changes; only the wrapper does. Read the first line and you know what you have — the extension (.pem, .key, none at all) tells you nothing. The certificate formats guide covers PEM and DER in general.
| First line | What it is | Who wants it |
|---|---|---|
-----BEGIN OPENSSH PRIVATE KEY----- | The openssh-key-v1 container: public key, private key and comment in one blob, encrypted with aes256-ctr and a bcrypt KDF when a passphrase is set. The default for every type since 7.8 (August 2018). | OpenSSH, PuTTY 0.75+, Go x/crypto/ssh, Paramiko 2.7+, the maintained JSch fork |
-----BEGIN RSA PRIVATE KEY----- | PKCS#1, the “traditional” PEM ssh-keygen wrote for RSA before 7.8 and still writes with -m PEM; encrypted copies carry Proc-Type: 4,ENCRYPTED and DEK-Info: AES-128-CBC,… headers. | OpenSSL, JSch 0.1.x, AWS-generated key pairs |
-----BEGIN EC PRIVATE KEY----- | SEC1, the traditional PEM form of an ECDSA key. | OpenSSL, TLS libraries |
-----BEGIN PRIVATE KEY----- | PKCS#8, algorithm-tagged, for RSA, EC and Ed25519 alike (-m PKCS8); encrypted, it begins -----BEGIN ENCRYPTED PRIVATE KEY-----. | OpenSSL 3, Java PKCS8EncodedKeySpec, most language libraries |
PuTTY-User-Key-File-3: | PuTTY’s .ppk; version 3 (Argon2) arrived in PuTTY 0.75 (2021), -2: is the older format. | PuTTY, Pageant, WinSCP, FileZilla |
ssh-ed25519 AAAA… | The public key line: type, Base64 blob, optional comment. | authorized_keys, GitHub, GitLab, cloud consoles |
---- BEGIN SSH2 PUBLIC KEY ---- | RFC 4716 public key; ssh-keygen -e exports, ssh-keygen -i imports. | Tectia and other SSH.com descendants, some SFTP appliances |
Whatever the wrapper, ssh-keygen -lf prints the same SHA256: fingerprint — a hash of the public blob — so a matching fingerprint before and after a conversion proves you still hold the same key. ssh-keygen -y -f id_ed25519 regenerates a lost .pub line from the private key.
When you have to convert, and how
PuTTY, WinSCP and FileZilla
These read .ppk only. In PuTTYgen, Load the OpenSSH private key, then Save private key; the reverse is Conversions → Export OpenSSH key. On Linux or macOS the same tool is a command:
puttygen ~/.ssh/id_ed25519 -O private -o id_ed25519.ppk # OpenSSH -> .ppk
puttygen id_ed25519.ppk -O private-openssh -o ~/.ssh/id_ed25519 # .ppk -> OpenSSHA PuTTY older than 0.75 refuses a version-3 file with PuTTY key format too new; update it, or save as version 2 under PuTTYgen’s Key → Parameters for saving key files.
Java, Python and other libraries stuck on PEM
The original JSch stopped at 0.1.55 in 2018 and fails on the container with com.jcraft.jsch.JSchException: invalid privatekey: [B@…; Paramiko before 2.7 (December 2019) says not a valid RSA private key file. Upgrading is the real fix — the maintained fork com.github.mwiede:jsch reads the container, Ed25519 included. Otherwise hand the library a PEM copy:
cp ~/.ssh/id_rsa ~/.ssh/id_rsa.pem
ssh-keygen -p -m PEM -N "" -f ~/.ssh/id_rsa.pem # -m PEM: PKCS#1 / SEC1; -m PKCS8: BEGIN PRIVATE KEY-p rewrites the file in place, hence the copy; -N "" means no passphrase. Two limits: ssh-keygen has no PEM writer for Ed25519 (it stops with unknown or unsupported key type), and a passphrase on PEM output gets the weak DEK-Info scheme from the table. Cloud consoles need no conversion at all — they take the public line — and an RSA key pair AWS generates downloads as a PKCS#1 .pem that ssh -i reads unchanged.
The SSH key converter does the same job without a shell and covers Ed25519. Paste or drop the key: an OPENSSH PRIVATE KEY block comes back as PKCS#8 (private-key.pem) with its public line and fingerprint; a PEM key — PKCS#8 plain or encrypted, PKCS#1 RSA with or without Proc-Type headers, unencrypted SEC1 EC — comes back as an OpenSSH container named id_ed25519, id_rsa or id_ecdsa, with fields for the PEM passphrase and a comment. It handles ssh-ed25519, ssh-rsa and ecdsa-sha2-nistp256/384; P-521 is refused with P-521 keys are not supported here — P-256, P-384, RSA and Ed25519 are., and a passphrase-protected OpenSSH key is refused with an instruction to run ssh-keygen -p -N "" -f <keyfile> first, because we chose not to run the bcrypt KDF in a tab. Output is always unencrypted. If a consumer insists on the PKCS#1 header, openssl pkey -in private-key.pem -traditional -out id_rsa.pem re-wraps it.
Passphrases: what they protect and what they do not
A passphrase encrypts the key file at rest. In the OpenSSH container that is aes256-ctr with a bcrypt-derived key over -a rounds (16 by default; ssh-keygen -a 100 slows every guess). In the old PEM format it is AES-128-CBC with a key from a single MD5 pass — fast to brute-force, one reason 7.8 changed the default. It defends against the file being copied — a stolen laptop, a backup, another account on a shared machine — and does nothing against malware running as you while the key is loaded.
An agent means typing it once per login: eval "$(ssh-agent -s)" then ssh-add ~/.ssh/id_ed25519. macOS keeps it in the keychain via ssh-add --apple-use-keychain with UseKeychain yes and AddKeysToAgent yes in ~/.ssh/config; Windows needs the OpenSSH Authentication Agent service running. ssh-add -c makes the agent confirm every use.
To change or remove one: ssh-keygen -p -f ~/.ssh/id_ed25519 asks for old and new, and an empty new passphrase removes it. For keys in PEM form the key passphrase tool does the same: it reads PKCS#8 plain or encrypted, PKCS#1 RSA with or without Proc-Type headers and unencrypted SEC1 EC, then either strips the passphrase to a plain PKCS#8 private-key.pem or, in Set / change passphrase mode, writes an AES-256-CBC encrypted PKCS#8 private-key-encrypted.pem. A wrong passphrase fails with Could not decrypt this key — check the passphrase.; an encrypted SEC1 EC key is the one form it cannot open. Feed it an OpenSSH container and it answers Could not read the private key — if it is encrypted, enter its passphrase. — your cue to use ssh-keygen -p instead.
A passphrase-less key on a CI runner is normal and needs the controls a passphrase would have given: a separate key per job or repository (GitHub deploy keys are read-only per repository by default), restrict,from="203.0.113.0/24",command="/usr/local/bin/deploy" in front of its authorized_keys line on your own servers, storage in the secret store rather than the repository, and rotation when people leave. Never reuse the key you log in with. The passwords guide covers what makes a passphrase worth the bcrypt rounds.
Generating a key pair without a terminal
The SSH key generator produces what ssh-keygen -N "" would: an unencrypted OpenSSH container downloaded as id_ed25519 or id_rsa, the matching .pub line and the SHA256 fingerprint. You choose Ed25519, RSA 4096 or RSA 2048 and optionally a comment (default onmydevice). Generating on your own device is the whole point: a web generator that mints the pair on its server has held your private key before you have, whereas this page calls the browser’s crypto.subtle.generateKey and the key exists nowhere but your tab until you download it. There is no passphrase option — the bcrypt KDF is too slow in a browser to ship — so add one afterwards with ssh-keygen -p -f ~/.ssh/id_ed25519. A browser without Ed25519 in WebCrypto stops with This browser cannot generate Ed25519 keys yet — pick RSA, or update the browser.
Install it like any key: mode 600 on the private file, then ssh-copy-id -i ~/.ssh/id_ed25519.pub user@host, or paste the public line under Settings → SSH and GPG keys on GitHub and test with ssh -T [email protected], which answers Hi user! You've successfully authenticated, but GitHub does not provide shell access.
How to tell which case you are in
head -1 ~/.ssh/id_ed25519 # the BEGIN line names the format (see the table)
sed -n 2p ~/.ssh/id_ed25519 | cut -c1-40 # OpenSSH container: the cipher name is right there
# b3BlbnNzaC1rZXktdjEAAAAABG5vbmU... -> "none": no passphrase
# b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHI... -> aes256-ctr: passphrase-protected
file ~/.ssh/id_rsa # "OpenSSH private key" or "PEM RSA private key"
ssh-keygen -lf ~/.ssh/id_ed25519.pub # 256 SHA256:... comment (ED25519)
ssh -v host 2>&1 | grep -E 'Offering|accepts' # "Offering public key:" but no "Server accepts key:" = server-side problemA public line names its type inside the Base64 too — Ed25519 blobs start AAAAC3NzaC1lZDI1NTE5AAAAI, RSA AAAAB3NzaC1yc2EAAAADAQAB, ECDSA P-256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTY— because the blob begins with the length-prefixed type name. Paste one into the converter and it returns only the fingerprint, the value a server’s auth.log records in Accepted publickey for user … ED25519 SHA256:….
Common mistakes and the errors they produce
Permissions 0644 for '/home/me/.ssh/id_ed25519' are too open.followed byThis private key will be ignored.chmod 600the key andchmod 700 ~/.ssh. The server has the same rule: withStrictModes yes(the default) a group-writable home,.sshorauthorized_keysis ignored andauth.logsaysAuthentication refused: bad ownership or modes for directory /home/me.Load key "/home/me/.ssh/id_rsa": invalid formatafter a Windows editor or a paste. The parser matches-----BEGIN OPENSSH PRIVATE KEY-----and-----END OPENSSH PRIVATE KEY-----together with the newline after each, so a CR before that newline, or no newline after the END line — the usual result of a CI secret pasted without its last line break — fails outright.sed -i 's/\r$//' id_rsaremoves carriage returns; write secrets back withprintf '%s\n' "$DEPLOY_KEY" > ~/.ssh/id_ed25519so the file ends with a newline. PEM keys with the same damage fail witherror in libcrypto.- Pasting the private key into authorized_keys. sshd ignores lines that are not
type base64 comment, so login still fails — and the secret now sits on the server. Treat that key as exposed, generate a new one, and letssh-copy-idcopy the.pubfor you. sign_and_send_pubkey: no mutual signature supported— an OpenSSH 8.8 or newer client, an RSA key, and a server that only understands the SHA-1ssh-rsasignature. Upgrade the server, or give it an Ed25519 key if it supports one. The last resort is a per-host entry in~/.ssh/config:Host legacy-box PubkeyAcceptedAlgorithms +ssh-rsa # PubkeyAcceptedKeyTypes before OpenSSH 8.5 HostkeyAlgorithms +ssh-rsa # only if the host key is also RSA-SHA1Received disconnect from 203.0.113.5 port 22:2: Too many authentication failures— the agent offered every key it holds and the server’sMaxAuthTries(6 by default) ran out. SetIdentitiesOnly yeswith an explicitIdentityFilefor that host.- Agent forwarding to a shared host.
ssh -AorForwardAgent yeslets anyone with root on the intermediate machine use your agent to authenticate as you while you are connected. UseProxyJump(ssh -J jump target, since OpenSSH 7.3) instead. - Converting a FIDO key.
[email protected]keys (OpenSSH 8.2) hold only a handle to a security key; the converter rejects them withUnsupported key type, and nothing can turn them into a PEM.
Do this
- Generate Ed25519 keys, one per device, with a comment naming the device; keep an RSA 4096 key only for a system that has no Ed25519.
- Identify any key by its first line, and an OpenSSH container’s passphrase state by the first 40 characters of its second line.
- Convert with
ssh-keygen -p -m PEMfor RSA and ECDSA, and with the converter for Ed25519 or when there is no shell — then compare fingerprints before and after. - Put a passphrase on every interactive key and load it through the agent; give unattended keys
restrictandcommand=options instead, plus their own rotation schedule. - Keep private keys at mode 600, with Unix line endings and a trailing newline, and copy public lines with
ssh-copy-id.
Frequently asked questions
Should I use Ed25519 or RSA for SSH?
Ed25519. It has been in OpenSSH since 6.5 (2014), ssh-keygen has generated it by default since 9.5, and GitHub and GitLab both recommend it. Keep an RSA 4096 key only for a specific old appliance or library that cannot accept anything else.
How do I convert an OpenSSH private key to PEM?
For RSA and ECDSA keys, ssh-keygen -p -m PEM -f id_rsa rewrites the file in place (copy it first). For any type, including Ed25519, paste the key into the SSH key converter on this site and download the PKCS#8 private-key.pem it produces; the SHA256 fingerprint stays the same because only the packaging changes.
Why does ssh say my key has an invalid format?
Almost always line endings or a missing final newline. The OpenSSH parser matches the BEGIN and END marker lines including the newline after them, so a file saved with Windows CRLF endings or pasted into a CI secret without its last newline fails with "invalid format". Strip carriage returns and make sure the END line ends the file.
Is it safe to remove the passphrase from my SSH key?
Only when the key is dedicated to unattended use, such as a deploy key. A passphrase protects the file if it is copied; without one, anyone who reads the file can use it, so compensate with a separate key per job, restrict and command= options in authorized_keys, 600 permissions and regular rotation.
Can I use the same SSH key on several computers?
You can copy the private key, but one key per device is better practice: if a laptop is lost you remove that one public key from authorized_keys and GitHub instead of rotating everywhere. Key files are tiny, and the comment field is there to label which machine each public line came from.
Tools used in this guide
Every one of these runs in your browser — the files you work on never leave your device.