Phone:

Hidden from the page source until you click: friction against scrapers, not a guarantee.

Email:

[email protected]

Category:

Security

Published:

Tags:
  • ssh
  • fido2
  • webauthn
  • security
  • hardware-key
  • linux

How to Set Up FIDO2 Hardware Key Authentication for SSH with Resident Keys

A passphrase-protected SSH key is only as good as the passphrase and the discipline of whoever typed it in. FIDO2 hardware keys move the private key material off the disk entirely: the actual signing key is generated and stays inside a secure element on a physical token, and OpenSSH just asks that token to sign a challenge each time. Since OpenSSH 8.2, this is built into ssh-keygen and sshd directly, no PAM modules or third-party agents required.

This walks through generating a resident FIDO2 key, which is the more useful of the two variants on offer, and explains why you'd want one over the default.

Resident vs non-resident keys

When you ask ssh-keygen for a -sk key type, it talks to the authenticator over CTAP2 and gets back a credential. By default this credential is non-resident: the authenticator hands you a "key handle" wrapping the actual private key, and ssh-keygen writes that handle into the usual private key file on disk (something like id_ecdsa_sk). To sign anything, you plug the token in, and it uses the handle to reconstruct which credential to use. The private key itself never leaves the device, but the handle file on disk is still load-bearing: lose it, and you've lost access to that identity even with the token in hand, because there's nothing left to tell the token which credential to use.

A resident (or "discoverable") key stores the credential on the authenticator's own flash, indexed by an application string. That means the token itself is self-sufficient: on a fresh install, with no key file anywhere, you can ask the token to hand back everything it's holding. This is the practical difference that matters for anyone who reinstalls machines, loses laptops, or wants one key working identically across several boxes.

Prerequisites

  • OpenSSH 8.2 or later on both client and server (check with ssh -V; FIDO2/U2F support and the ecdsa-sk/ed25519-sk key types landed together in that release).
  • A distribution build of OpenSSH linked against libfido2. Debian, Ubuntu, Fedora and Arch all ship this by default; a from-source build needs libfido2 present at compile time.
  • A genuine FIDO2 CTAP2 authenticator that supports resident keys and a PIN, e.g. a Yubikey 5 series (firmware 5.2.3+) or a SoloKeys/Nitrokey 3. Older U2F-only tokens will do non-resident ecdsa-sk but can't hold resident credentials.

Step 1: confirm the hardware is visible

The fido2-token tool from libfido2 is the quickest sanity check:

fido2-token -L
fido2-token -I /dev/hidraw3

-I reports the authenticator's capabilities, including whether it supports resident credentials and, usefully, how many discoverable credential slots it has left. Older Yubikeys cap out at around 25 resident credentials shared across everything on the device, not just SSH: if you also use the same key for WebAuthn passkey logins on websites, those eat into the same pool.

If fido2-token reports permission errors rather than device errors, it's almost always a udev rules problem: your distribution's libfido2 package normally installs rules that grant the active login session access to the raw HID device, but on a minimal or manually assembled system you may need to add one yourself.

Step 2: set a PIN on the authenticator

Resident credentials require the authenticator to have a PIN configured, this is a FIDO2 protocol requirement, not an OpenSSH choice, because a discoverable credential can otherwise be enumerated by anyone with physical access to the key:

fido2-token -S /dev/hidraw3

This prompts for a new PIN and writes it to the token. Yubikeys also support this through ykman fido access change-pin if you'd rather use vendor tooling.

Step 3: generate the resident key

ssh-keygen -t ed25519-sk \
  -O resident \
  -O verify-required \
  -O application=ssh:workstation \
  -C "andy@workstation" \
  -f ~/.ssh/id_ed25519_sk_workstation

The options that matter here:

  • -O resident stores the credential on the authenticator itself rather than only wrapping it into the local private key file.
  • -O verify-required forces PIN entry (user verification) on every signature, not just a touch (user presence). Without it, physical possession of the token plus a touch is sufficient, which is fine if your threat model is "opportunistic thief" but weaker against "someone who has stolen the token specifically to use it".
  • -O application=ssh:workstation namespaces the credential. The default is just ssh:, but giving each identity its own suffix lets you keep several distinct SSH credentials, e.g. one per machine or one for work versus personal use, resident on a single physical key.

If the authenticator doesn't support EdDSA over CTAP2, ssh-keygen will fail generating ed25519-sk; fall back to ecdsa-sk (NIST P-256), which has broader support going back to the original U2F-era tokens.

You'll be prompted to touch the token, and, because of verify-required, to enter the PIN. The command produces a normal-looking key pair on disk, but the private key file is not a private key in the traditional sense: open it and you'll find a wrapped credential reference and the stored options, not usable ECDSA or Ed25519 key material. The actual signing key never leaves the secure element.

Step 4: install the public key as usual

Nothing changes here versus any other SSH key. Copy the .pub file's contents into the server's ~/.ssh/authorized_keys:

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

Logging in will prompt for a touch (and PIN, given verify-required) exactly as key generation did.

Recovering the credential on a new machine

This is where resident keys earn their keep. Wipe the laptop, buy a new one, whatever: plug the token into the fresh machine and pull the credentials straight back off it, no backup of the key files required:

cd ~/.ssh
ssh-keygen -K

-K asks the connected authenticator for its PIN, enumerates every resident credential it's holding, and writes out a private/public key pair for each one (named id_ed25519_sk_rk1 and so on). Rename them to something sensible afterwards if you're recovering more than one. This only works because the credential, and the application string identifying it, lived on the token all along; the files you had on the old disk were never the authoritative copy.

Things worth deciding deliberately

verify-required versus touch-only is a real tradeoff, not a default to leave on autopilot: PIN entry on every login is friction you'll feel daily, in exchange for meaningfully raising the bar if the token itself is stolen. Enable it for anything guarding production access; touch-only is a reasonable compromise for lower-stakes personal boxes.

Agent forwarding still deserves its usual caution. Touch-per-signature stops a compromised remote host from silently signing things in the background, but for the duration of a forwarded session it can still request signatures while you're actively touching the key for legitimate reasons, so don't forward the agent to hosts you don't trust.

And enrol a second physical key before you commit to this as your only authentication method. A resident FIDO2 credential with no password fallback is a genuine single point of failure if the token is lost, dropped down a drain, or simply dies; add a second key's public credential to authorized_keys on every server up front, not after the first one fails.

On a system that doesn't build OpenSSH with native libfido2 support, there's an escape hatch worth knowing about: SSH_SK_PROVIDER can point at an external shared library implementing the same middleware interface, which is how some vendor-supplied or macOS setups plug in FIDO2 support without OpenSSH doing it directly. On a mainstream Linux distribution you're unlikely to need it.