Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SSH with FIDO keys (ed25519-sk / ecdsa-sk)

Hardware-backed SSH keys. The private key is generated on the device and never leaves it; the file on disk is only a handle that points at it. Logging in takes one touch (and a PIN, if you ask for one). This is the OpenSSH “security key” (-sk) feature — the device is a FIDO2 authenticator, and RS-Key supports both key types it can use:

Key typeAlgorithmUse it when
ed25519-skEd25519 (EdDSA)the default — smallest, fastest
ecdsa-skNIST P-256 (ES256)a server or old client rejects ed25519-sk

Requirements

  • OpenSSH 8.2+ for -sk keys (8.3+ to download resident keys with -K).
  • A FIDO middleware: distro OpenSSH links libfido2; check with ssh -Q key | grep sk.
  • macOS: Apple’s /usr/bin/ssh ships without FIDO support and fails with Permission denied before touching the device. Use Homebrew OpenSSH:
    brew install openssh
    export PATH="/opt/homebrew/opt/openssh/bin:$PATH"   # ahead of /usr/bin
    
  • Linux: OpenSSH links libfido2 almost everywhere; you only need the FIDO udev rules — see linux.md. FIDO is local to wherever ssh runs, so logging in to a remote box needs nothing special there.

Enroll

ssh-keygen -t ed25519-sk -f ~/.ssh/id_ed25519_sk -C "you@laptop"
# → enter the FIDO PIN if one is set, then touch the button

The -C comment is free text that ends up in the .pub and on the server — handy for telling keys apart. Two files appear:

  • id_ed25519_sk — the handle. Not a private key; useless without the physical device. Copy it (and the .pub) to every machine you ssh from — the device is the second factor, the file is just a pointer.
  • id_ed25519_sk.pub — the public key, for authorized_keys.

Then install it on a server:

ssh-copy-id -i ~/.ssh/id_ed25519_sk.pub you@server
ssh -i ~/.ssh/id_ed25519_sk you@server      # one touch

Enrollment options (-O)

Pass -O flags at ssh-keygen time to shape the credential:

-O optionEffect
residentstore the key on the device so it can be downloaded later (see below)
verify-requireddemand the FIDO PIN on every login, not just a touch
application=ssh:NAMEtag the credential (default ssh:); a distinct string is a distinct key
user=NAMEuser handle stored with a resident key (for listing/telling them apart)
no-touch-requiredmark the key as not needing a touch — see the note below
write-attestation=FILEsave the enrollment attestation for later verification
challenge=FILEuse a fixed challenge (for reproducible attestation)
# PIN on every login, and store the key on the device:
ssh-keygen -t ed25519-sk -O resident -O verify-required \
    -O application=ssh:work -f ~/.ssh/id_work_sk

no-touch-required does nothing useful on RS-Key. The default (touch) build always polls the button on every assertion — the firmware does not honor up:false. The flag still marks the credential, but you will be asked to touch regardless. The touch is the point; enroll without it.

PIN and touch: what to expect

RS-Key follows the standard FIDO2 flow, the same as a YubiKey: the PIN unlocks a session token silently — no touch for the PIN itself — and then each operation takes one touch.

ActionPINTouch
Enroll (ssh-keygen -t …-sk)onceonce*
Log in, normal keyonce
Log in, verify-required keyonceonce

* You only touch twice at enrollment when several FIDO devices are plugged in at once: the first touch is a CTAP “selection” gesture (which key did you mean?), the second authorizes the key creation. With one device connected it is a single touch.

So a single login asks for the PIN at most once. If you ever see the PIN prompt twice in one action, two separate operations are running — usually the key is offered by both ssh-agent and an IdentityFile (add IdentitiesOnly yes), or git push opened two SSH channels (use ControlMaster / ControlPersist, see the git guide). A real YubiKey behaves identically in those setups — it is the client, not the device.

Resident (discoverable) keys

-O resident stores the key handle on the device itself, so you can recover it onto any machine later instead of carrying the file:

ssh-keygen -K                      # download handles into the current dir (PIN)
# → writes id_ed25519_sk_rk[...]  and the matching .pub
ssh-add -K                         # load resident keys straight into the agent
rsk fido list-passkeys             # see what's stored on the device

Resident keys cost one of the device’s 256 discoverable-credential slots. For most people the non-resident default plus a seed backup serves better: non-resident keys are re-derivable from the seed, so a restored board logs in with the same handle files — no slot used, nothing to download. Reach for resident keys when you want to walk up to a fresh machine with only the device in your pocket.

ssh-agent and ~/.ssh/config

Add the key to the agent so you are not retyping -i:

ssh-add ~/.ssh/id_ed25519_sk       # non-resident: add the handle file
ssh-add -K                         # resident: pull from the device
ssh-add -l                         # list loaded keys

A config block makes plain ssh host use the right key (and the Homebrew binary on macOS):

# ~/.ssh/config
Host server
    HostName server.example.com
    User you
    IdentityFile ~/.ssh/id_ed25519_sk
    IdentitiesOnly yes

IdentitiesOnly yes stops the agent from offering every other key first — worth it so each connection prompts for exactly one touch.

Server side

The .pub goes in ~/.ssh/authorized_keys like any key. You can also pin requirements there, independent of how the key was enrolled:

# authorized_keys — require the FIDO PIN for this key (touch is omitted = required)
verify-required sk-ssh-ed25519@openssh.com AAAA... you@laptop

sshd enforces verify-required from OpenSSH 8.4+; older servers accept the key but skip the check. A key enrolled verify-required always asks for the PIN on the client regardless of the server. (Adding no-touch-required here would relax the touch requirement — but RS-Key touches anyway, as above.)

Signing git commits

The same key signs git commits and tags (no GPG needed) — see the git guide. The short version:

git config gpg.format ssh
git config user.signingkey ~/.ssh/id_ed25519_sk.pub
git config commit.gpgsign true     # one touch per commit

Using the OpenPGP AUT slot instead

If you already run an OpenPGP key on the card, its authentication subkey doubles as an SSH key via gpg-agent — a different path that needs no -sk support in the client. See openpgp.md (gpg --export-ssh-key, enable-ssh-support).

Troubleshooting

  • Permission denied instantly on macOS → you are on /usr/bin/ssh; use the Homebrew binary (above).
  • Key enrollment failed: requested feature not supported → the client lacks ed25519-sk middleware; install libfido2 / Homebrew OpenSSH, or fall back to -t ecdsa-sk.
  • device not found / no prompt → FIDO udev rules missing (linux.md), or a browser / gpg-agent is holding the device — close it and retry.
  • Asks for a PIN you never set → some client builds require a PIN to enroll; set one with rsk fido set-pin and retry.
  • sign_and_send_pubkey: signing failed on login → the wrong device is plugged in, or the key is resident on a device you reset. Re-plug the right key, or ssh-add -K again.
  • After a factory FIDO reset, old id_*_sk files stop working — the seed they derive from is gone. Re-enroll, or restore the seed first so the same handles work again.