TechEarl

SSH Keys With a YubiKey (FIDO2 / U2F)

Generate a hardware-backed SSH key on a YubiKey with one ssh-keygen command. How FIDO2/U2F SSH keys work, the difference between ed25519-sk and ecdsa-sk, resident keys, and the firmware and OpenSSH versions you need.

Ishan Karunaratne⏱️ 11 min readUpdated
Share thisCopied
Create a hardware-backed SSH key on a YubiKey with ssh-keygen -t ed25519-sk. How FIDO2/U2F SSH keys work, ed25519-sk vs ecdsa-sk, resident keys, and the OpenSSH and firmware versions you need.

A FIDO2 SSH key on a YubiKey is one command:

bash
ssh-keygen -t ed25519-sk

The -sk suffix is the whole story: it stands for "security key". When you run that, OpenSSH talks to the YubiKey, the key's LED starts blinking, you tap it, and you end up with a keypair where the private half physically cannot leave the hardware. What lands in ~/.ssh is not the private key, it is a handle that is useless without the YubiKey plugged in. Steal the laptop, copy the file, and you still cannot authenticate. That is the entire point: the secret lives in a chip you can pull out of the USB port.

This needs OpenSSH 8.2 or newer on both ends and a FIDO2 key. I cover the version traps below, because they are where people actually get stuck.

Generate the key

Plug in the YubiKey first, then run:

bash
ssh-keygen -t ed25519-sk -C "you@example.com"

A real run looks like this:

code
Generating public/private ed25519-sk key pair.
You may need to touch your authenticator to authorize key generation.
Enter file in which to save the key (/home/techearl/.ssh/id_ed25519_sk):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/techearl/.ssh/id_ed25519_sk
Your public key has been saved in /home/techearl/.ssh/id_ed25519_sk.pub

Between "touch your authenticator" and the saved paths, the YubiKey blinks and waits for a physical tap. No tap, no key. That tap is user presence: proof a human is at the machine, not a script.

You now have two files, but they are not what ssh-keygen normally produces. id_ed25519_sk is a key handle plus metadata, not a usable private key on its own. id_ed25519_sk.pub is the public key you install on servers, exactly like any other public key. The crucial difference from a software key: copying id_ed25519_sk to another machine gets an attacker nothing, because the actual private scalar never left the YubiKey's secure element.

Run ssh-keygen as your normal user. It writes to your own ~/.ssh, so no sudo. (Using sudo here makes the handle root-owned and unreadable to you, same mistake as with a regular ssh-keygen run.)

ed25519-sk or ecdsa-sk?

Two algorithms carry the -sk suffix. Pick ed25519-sk if your YubiKey supports it, fall back to ecdsa-sk if it does not.

bash
ssh-keygen -t ed25519-sk     # preferred
ssh-keygen -t ecdsa-sk       # fallback for older keys

The catch is firmware. ed25519-sk needs a YubiKey on firmware 5.2.3 or later. Anything older only does ecdsa-sk. Check your firmware with:

bash
ykman info

If ssh-keygen -t ed25519-sk returns Key enrollment failed: requested feature not supported, that is almost always old firmware, and ecdsa-sk will work instead. YubiKey firmware is burned in at the factory and cannot be upgraded, so an older key is stuck on ecdsa-sk for life. That is not a real security problem; it is just a less modern curve.

AlgorithmCurveYubiKey firmwareWhen to pick it
ed25519-skEd255195.2.3 or laterDefault. Modern curve, short keys, the one I reach for.
ecdsa-skNIST P-2565.1.x and laterOnly when the key's firmware is too old for ed25519-sk.

Both require OpenSSH 8.2+ on the client that generates and uses the key, and on the server you log in to (the server has to understand the sk key type in authorized_keys).

One trap that catches people on minimal Linux installs: OpenSSH talks to the YubiKey through a middleware library, libfido2, and the ssh-sk-helper binary that ships with OpenSSH. If ssh-keygen -t ed25519-sk fails with something like Couldn't get key types: error in libcrypto or a flat "provider" error even though ssh -V shows 8.2+, you are usually missing libfido2 (sudo apt install libfido2-1 on Debian/Ubuntu, libfido2 on Fedora/Arch). Distro-packaged OpenSSH normally pulls it in; hand-built or stripped-down images often do not.

Resident keys: surviving a fresh laptop

By default the handle file matters: lose id_ed25519_sk and you cannot use the key even with the YubiKey, because the handle is half of what is needed. A resident (discoverable) key changes that. It stores the handle on the YubiKey itself, so you can walk up to any machine, plug the key in, and pull the credential back down.

bash
ssh-keygen -t ed25519-sk -O resident

On a fresh machine, retrieve the resident keys straight off the hardware:

bash
ssh-keygen -K

That writes id_ed25519_sk_rk and its .pub into the current directory. The trade-off is real: resident keys consume one of the YubiKey's limited FIDO2 credential slots, and anyone who has both the unlocked YubiKey and (if set) its FIDO2 PIN can extract them. They also need firmware 5.2.3 or later; older 5.1.x keys do FIDO2 but not discoverable credentials. For most people the non-resident default is the right call. Reach for -O resident only when "I need to log in from a machine that has never seen my key handle" is a genuine requirement, for example a recovery scenario or a shared jump box.

Useful generation options

ssh-keygen exposes the FIDO2 options through -O:

OptionEffect
-O residentStore the handle on the key itself (discoverable).
-O verify-requiredRequire a PIN (or biometric) in addition to the touch.
-O no-touch-requiredDrop the touch requirement (weakens the guarantee; most servers reject it).
-O application=ssh:workTag the credential with a custom application string to keep multiple keys apart.

-O verify-required is the one I add for anything sensitive. It means a stolen-and-plugged-in YubiKey is not enough; the attacker also needs the FIDO2 PIN. The server can enforce it too, with verify-required in authorized_keys, so the requirement is checked on both ends. Note that this option needs OpenSSH 8.4 or newer (the per-use PIN requirement landed two releases after the base sk key types), and a YubiKey on firmware 5.2.3 or later; firmware 5.1.x does not support it. You set the PIN itself with ykman fido access change-pin; without one set, verify-required cannot enroll.

Install the public key and log in

Identical to any other SSH key. Append id_ed25519_sk.pub to the server's authorized_keys:

bash
ssh-copy-id -i ~/.ssh/id_ed25519_sk.pub user@server

Then connect. The difference shows up at login: the YubiKey blinks and SSH waits for your tap before it will authenticate.

bash
ssh user@server
# Confirm user presence for key ED25519-SK ...
# (LED blinks, tap the YubiKey)

A few things to keep straight once it works:

When not to bother

Hardware keys are not free in friction, and there are spots where a plain Ed25519 software key is the better tool:

  • Unattended automation. CI runners, cron jobs, and deploy bots cannot tap a YubiKey. A FIDO2 key requiring user presence is the wrong choice for anything that must authenticate without a human present. Use a scoped software key (or short-lived certificates) there.
  • Servers older than OpenSSH 8.2. The server side has to understand sk key types. A box stuck on an ancient OpenSSH will reject the public key outright. Check ssh -V on the server before committing.
  • You only have one key. A single YubiKey is a single point of failure: lose it and you are locked out of everything that trusts it. Enroll a backup key on every account from day one, the same way you would with any FIDO2 setup.

For everyday interactive logins from a laptop you carry around, though, a FIDO2 SSH key is the strongest practical setup: the private key is in silicon you control, and every login needs a deliberate physical tap.

It stands for "security key". ssh-keygen -t ed25519-sk generates an Ed25519 key whose private half is created and stored inside a FIDO2 authenticator like a YubiKey. What gets written to ~/.ssh is a key handle, not the private scalar, so the secret never touches your disk.

The same suffix exists for ECDSA as ecdsa-sk, for keys whose firmware is too old for Ed25519.

OpenSSH 8.2 or newer, on both the client that generates and uses the key and the server you log in to. FIDO/U2F support landed in the 8.2 release in February 2020. You can check with ssh -V. The server has to understand the sk key type in authorized_keys, so an older box will reject the public key even if your laptop is current.

ed25519-sk requires YubiKey firmware 5.2.3 or later. Earlier hardware only supports ecdsa-sk. YubiKey firmware cannot be upgraded after manufacture, so an older key is permanently limited to ecdsa-sk. Run ykman info to read the firmware version. The ECDSA variant is still hardware-backed and perfectly usable; it just uses the NIST P-256 curve instead of Ed25519.

A resident (discoverable) key stores the key handle on the YubiKey itself, generated with ssh-keygen -t ed25519-sk -O resident. You can then plug the key into any fresh machine and pull the credential back with ssh-keygen -K, with no handle file needed beforehand. Most people do not need this: the non-resident default is fine and does not consume a credential slot. Use resident keys only when logging in from machines that have never seen your handle file is a real requirement.

You copy the handle file and public key, but not the private key, because the private key never leaves the YubiKey. On the new machine the handle is only usable with that physical YubiKey plugged in. If you generated a resident key, you do not even need to copy files: run ssh-keygen -K to retrieve the credential directly from the hardware.

Yes. ssh-add ~/.ssh/id_ed25519_sk loads the handle into the agent like any key, so you are not retyping the handle passphrase all day. The touch requirement is not cached, though: the agent still asks for a physical tap on every authentication, because user presence is asserted by the hardware per signature, not held in the agent. That is the whole security property, so it is working as intended, not a bug. For a resident key you do not have the handle file on a fresh box, so run ssh-keygen -K first, then ssh-add the retrieved file.

You cannot back up the private key itself; that is the point, it never leaves the hardware, so there is nothing to copy. The backup strategy is a second physical key. Generate a separate FIDO2 key on a spare YubiKey and add both public keys to every server's authorized_keys from day one. If you lose the primary, the backup still authenticates, and you revoke the lost key by removing its public line. This is the same enroll-a-backup discipline FIDO2 uses everywhere; treat the SSH key no differently. Pairing it with disabled password auth means a lost key never falls back to a weaker login.

See also

Sources

Authoritative references this article was fact-checked against.

TagsSSHYubiKeyFIDO2U2Fssh-keygenEd25519Hardware Security KeySecurity

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Software Systems Architect · Senior Software Engineer · Engineering Leadership

Software systems architect and senior software engineer with more than two decades designing, building, and running production software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Now a CTO, though what I write here is drawn from the full arc of that work, across architecture, engineering, and operations, not any single job.

Keep reading

Related posts

Bind separate SSH keys to separate hosts in ~/.ssh/config with IdentityFile and IdentitiesOnly so the correct key is offered every time.

Use Multiple SSH Keys with ~/.ssh/config

Run separate SSH keys for work, personal, and GitHub by binding each to its host in ~/.ssh/config with IdentityFile and IdentitiesOnly, so the right key is always offered.

How to set up push-to-deploy with GitHub Actions: a CI/CD workflow that builds and deploys on every push to main.

Push-to-Deploy with GitHub Actions

A beginner-friendly intro to CI/CD with GitHub Actions: a real workflow YAML that builds and deploys on every push to main, plus how to handle secrets safely.

Clean responsive images from ACF Image fields. ID + wp_get_attachment_image for auto srcset, manual srcset control, picture element for art direction.

Proper Responsive Images with ACF Image Fields

The cleanest pattern for responsive images from an ACF Image field: ID return format plus wp_get_attachment_image, which produces a complete srcset and sizes attribute from registered image sizes. Plus the manual srcset pattern when you need control.