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 type | Algorithm | Use it when |
|---|---|---|
ed25519-sk | Ed25519 (EdDSA) | the default — smallest, fastest |
ecdsa-sk | NIST P-256 (ES256) | a server or old client rejects ed25519-sk |
Requirements
- OpenSSH 8.2+ for
-skkeys (8.3+ to download resident keys with-K). - A FIDO middleware: distro OpenSSH links
libfido2; check withssh -Q key | grep sk. - macOS: Apple’s
/usr/bin/sshships without FIDO support and fails withPermission deniedbefore touching the device. Use Homebrew OpenSSH:brew install openssh export PATH="/opt/homebrew/opt/openssh/bin:$PATH" # ahead of /usr/bin - Linux: OpenSSH links
libfido2almost everywhere; you only need the FIDO udev rules — see linux.md. FIDO is local to whereversshruns, 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, forauthorized_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 option | Effect |
|---|---|
resident | store the key on the device so it can be downloaded later (see below) |
verify-required | demand the FIDO PIN on every login, not just a touch |
application=ssh:NAME | tag the credential (default ssh:); a distinct string is a distinct key |
user=NAME | user handle stored with a resident key (for listing/telling them apart) |
no-touch-required | mark the key as not needing a touch — see the note below |
write-attestation=FILE | save the enrollment attestation for later verification |
challenge=FILE | use 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-requireddoes nothing useful on RS-Key. The default (touch) build always polls the button on every assertion — the firmware does not honorup: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.
| Action | PIN | Touch |
|---|---|---|
Enroll (ssh-keygen -t …-sk) | once | once* |
| Log in, normal key | — | once |
Log in, verify-required key | once | once |
* 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 deniedinstantly on macOS → you are on/usr/bin/ssh; use the Homebrew binary (above).Key enrollment failed: requested feature not supported→ the client lacksed25519-skmiddleware; installlibfido2/ Homebrew OpenSSH, or fall back to-t ecdsa-sk.device not found/ no prompt → FIDO udev rules missing (linux.md), or a browser /gpg-agentis 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-pinand retry. sign_and_send_pubkey: signing failedon login → the wrong device is plugged in, or the key is resident on a device you reset. Re-plug the right key, orssh-add -Kagain.- After a factory FIDO reset, old
id_*_skfiles stop working — the seed they derive from is gone. Re-enroll, or restore the seed first so the same handles work again.