
Hardware-Backed SSH: Configuring sk-ssh-ed25519 with OpenSSH 8.2+ and FIDO2 User Presence
secure-access
This series is part of WarnHack's Topical Authority research.
12 Articles
Understanding OpenSSH FIDO2 Hardware Key Authentication
I plugged in a newly unboxed hardware key, ran ssh-keygen -t ed25519-sk, and immediately got slapped with this:
$ ssh-keygen -t ed25519-sk -f ~/.ssh/id_ed25519_sk
Generating public/private ed25519-sk key pair. You may need to touch your authenticator to authorize key generation. Key enrollment failed: provider "internal" returned failure code -1
No prompt, no LED flash, just an instant failure code. Classic Friday afternoon.
OpenSSH 8.2 introduced native FIDO2 / U2F support via two security key types: [email protected] and [email protected]. Instead of relying on a local PKCS#11 shared library like we used to do with PIV tokens, OpenSSH speaks directly to FIDO2 authenticators over HID using libfido2.
The mental model here trips people up: the "private key" file written to your ~/.ssh/ directory is not a standard private key. It is a key handle (or credential ID), alongside the public key and flags. When you initiate an SSH handshake, OpenSSH hands that key handle to the physical security key. The token's secure element checks the handle, demands physical user presence (a touch) or user verification (a PIN), and computes the signature entirely on-chip. If an attacker exfiltrates your ~/.ssh/id_ed25519_sk file from your laptop, it is useless to them without the physical hardware token.
Key Types Explained: ecdsa-sk vs. ed25519-sk
Always default to ed25519-sk unless your token hardware literally will not let you. Ed25519 gives you constant-time operations, immune to cache-timing attacks, and has smaller key sizes. The catch? Ed25519 support in FIDO2 was standardized later (FIDO 2.1 / CTAP 2.1). Older security keys—including YubiKey 5 series running firmware prior to 5.2.3—only implement standard NIST P-256 (ecdsa-sk).
| Key Type | Spec / Standard | Hardware Compatibility | Security Profile |
|---|---|---|---|
[email protected] |
CTAP 2.1 / Ed25519 | Modern keys (YubiKey 5.2.3+, SoloKey, Nitrokey 3) | Preferred. Constant-time, small footprint, no NIST curves. |
[email protected] |
CTAP 2.0 / FIDO U2F | Almost every U2F/FIDO2 token made in the last decade | Fallback for legacy keys. NIST P-256 curve implementation. |
Prerequisites for Using FIDO2 Security Keys with SSH
Both sides of the connection matter here. The client generates and signs; the server verifies.
- OpenSSH Client: Needs to be version 8.2 or newer, compiled with
--with-security-key-builtin(most modern distribution packages include this by default). - OpenSSH Server (sshd): Must be version 8.2 or newer. If you try to drop an
sk-ssh-ed25519public key onto an old Ubuntu 18.04 or CentOS 7 box running OpenSSH 7.4,sshdwill fail to parse the line inauthorized_keysand silently ignore it. - System Libraries: You need
libfido2installed on your local workstation.
Required System Libraries and udev Rules
On Linux, that initial error I hit usually boils down to missing raw HID permissions. When you plug in a USB authenticator, the kernel maps it under /dev/hidraw*. By default, root owns those devices, leaving non-root users locked out.
Install the library and standard udev rules:
# Debian / Ubuntusudo apt install libfido2-1 u2f-host libfido2-dev
# Fedora / RHEL
sudo dnf install libfido2 libfido2-devel
If your distribution doesn't include the udev rules package (often called libfido2-udev or yubikey-udev-rules), you need a rule in /etc/udev/rules.d/70-u2f.rules to set the TAG+="uaccess" property so the logged-in desktop user can access the device:
# /etc/udev/rules.d/70-u2f.rules
ACTION=="add|change", KERNEL=="hidraw*", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0407", TAG+="uaccess"
Reload udev, unplug the key, and plug it back in:
sudo udevadm control --reload-rules && sudo udevadm trigger
How to Generate OpenSSH Keys with FIDO2 Hardware Tokens
Once permissions are sorted, key generation takes a few seconds.
Generating Standard FIDO2 SSH Keys with ssh-keygen
For a basic keypair tied to the token in your USB port:
$ ssh-keygen -t ed25519-sk -O application=ssh:infra -C "primary-key-$(date +%F)" -f ~/.ssh/id_ed25519_sk
Generating public/private ed25519-sk key pair. You may need to touch your authenticator to authorize key generation. Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/user/.ssh/id_ed25519_sk Your public key has been saved in /home/user/.ssh/id_ed25519_sk.pub
The token lights up. Tap it. The resulting id_ed25519_sk file can (and should) still be encrypted with a local passphrase. Layering a passphrase on top of physical hardware prevents an attacker who borrows your laptop while you step away for coffee from using the key even if the USB token remains plugged in.
Configuring Resident Keys for Portability
Standard SK keys leave you stranded if you hop between laptops or re-image your OS: you need that local stub file. Resident keys (discoverable credentials) solve this by saving the key handle directly into the internal storage of the FIDO2 key.
$ ssh-keygen -t ed25519-sk -O resident -O application=ssh:workstation -f ~/.ssh/id_ed25519_sk_resident
Generating public/private ed25519-sk key pair. Enter PIN for authenticator: You may need to touch your authenticator to authorize key generation.
When you sit down at a new machine that has OpenSSH 8.2+, you do not need to copy any files over. You simply plug in your hardware key and dump the handles back onto disk:
$ ssh-keygen -K
Enter PIN for authenticator: You may need to touch your authenticator to authorize key extraction. Saved id_ed25519_sk_rk to current directory
Your hardware key has limited storage for resident keys (usually 25–100 slots depending on the model), so don't treat this as an unlimited bucket.
Enforcing User Verification with FIDO2 PIN
FIDO2 makes a sharp distinction between User Presence (UP) and User Verification (UV):
- User Presence: You tapped the capacitive sensor. Proves a human is physically at the machine. Does not prove which human.
- User Verification: You entered the token's hardware PIN (or scanned a fingerprint on biometric keys) and tapped the sensor. Proves identity to the device.
To force PIN verification on every authentication, add -O verify-required during generation:
$ ssh-keygen -t ed25519-sk -O verify-required -f ~/.ssh/id_ed25519_sk_uv
If you set this, the key handle enforces that the client must negotiate UV with the token. If the client doesn't prompt for a PIN, the token outright refuses to sign.
Deploying and Configuring FIDO2 Keys on Remote Servers
The public key looks slightly different than a regular ed25519 string:
$ cat ~/.ssh/id_ed25519_sk.pub
[email protected] AAAAGnNrLXNzaC1lZDI1NTE5QG9wZW5zc2guY29tAAAAIInq9... user@box
Paste this line into ~/.ssh/authorized_keys on your destination hosts just like any other key. But you can also set server-side policy on that specific line.
Restricting User Presence Requirements on the Host
Suppose you want to guarantee that nobody on a server uses an SSH client configured to skip presence checks. You can prefix options directly in the target's authorized_keys file:
# Enforce user verification (PIN/biometrics) server-sideverify-required [email protected] AAAAGnNrLX... admin-hardware-key
# Default behavior: requires touch (user presence)
[email protected] AAAAGnNrLX... admin-hardware-key
If an SSH client attempts to sign using a key configured with no-touch-required, but the server line specifies verify-required, the server drops the connection instantly.
Restricting Algorithms in sshd_config
To ban non-hardware keys across your fleet, enforce it globally in /etc/ssh/sshd_config:
# Only allow hardware-backed FIDO2 keys
PubkeyAcceptedAlgorithms [email protected],[email protected]
Test your config before reloading: sshd -t. If you lock yourself out here without an active session, you'll be spending your evening pulling out the out-of-band console.
Managing FIDO2 Keys with ssh-agent
Standard ssh-agent workflows work fine with OpenSSH FIDO2 hardware keys, but the behavior has distinct quirks.
If you generated a resident key on the token, you don't even need the file on disk to populate your agent. Just load it directly:
$ ssh-add -K
Enter PIN for authenticator: Identity added: /tmp/ssh-XXXXXX (ssh:workstation)
Now the agent holds the handle. When an SSH connection triggers, your terminal hangs, the token flashes, and you tap it.
Agent Forwarding Realities
People often ask: "If I forward my agent (ssh -A), can a compromised jump host use my hardware key without my knowledge?"
No—assuming you didn't deliberately override touch requirements. The private key never lives in ssh-agent memory. The agent only holds the handle. When the remote box sends an auth challenge back through the forwarded agent socket to your laptop, your local hardware token still demands physical touch for that specific challenge. If someone scripts an automated loop on the jump host to abuse your forwarded socket, your token will start blinking frantically on your desk. Don't touch it.
Backup Keys and Failure Modes
A pure hardware-enforced setup introduces a real risk: hardware failure or loss. Security keys go through the wash, get crushed in car doors, or simply disappear at airport security.
Do not maintain a secret fallback password or an un-backed standard RSA key in your drawer to save yourself; that defeats the entire point when working to replace shared SSH keys and eliminate unprotected credentials. Instead, provision two identical keys simultaneously:
- Plug in Key A (Primary). Generate
id_ed25519_sk_primary. - Plug in Key B (Backup). Generate
id_ed25519_sk_backup. - Add both public keys to your target servers'
authorized_keys. - Put Key B in a fireproof safe.
# authorized_keys
[email protected] AAAAGnNr... primary-yubikey-blue [email protected] AAAAGnNr... backup-yubikey-black
If you don't enroll the backup key upfront, you will eventually find yourself booted off your own machines.
Troubleshooting Common OpenSSH FIDO2 Errors
"Key enrollment failed: invalid format"
You tried to run ssh-keygen -t ed25519-sk, but your hardware token only supports FIDO/U2F CTAP 2.0 without Ed25519 extensions. Fall back to ECDSA:
$ ssh-keygen -t ecdsa-sk -f ~/.ssh/id_ecdsa_sk
"sign_and_send_pubkey: signing failed: agent refused operation"
Usually happens when the token requires a PIN, but you're operating in an environment without a pinentry agent or graphical askpass utility configured. Set your pinentry properly or run ssh directly from an interactive TTY.
Server rejects key despite correct authorized_keys
Check the server's auth log:
sudo journalctl -u ssh -e | grep -i "userauth_pubkey"
If you see userauth_pubkey: unsupported public key algorithm: [email protected], the target server is running an OpenSSH version older than 8.2. You cannot use native FIDO2 keys on that host without upgrading sshd.
Next thing to look at: chaining hardware-backed keys into an SSH Certificate Authority using ssh-keygen -s, specifically whether verify-required can be cleanly baked into the certificate extensions without breaking non-interactive CI pipelines. I suspect it can't, but that's an experiment for next week.
Explore Topics
Replace Shared SSH Keys with Browser Access
WarnHack Terminal: browser SSH, role-based access, CloudRelay session & command audit — Teleport/StrongDM alternative for lean teams.
Stay Ahead of Threats
Get the latest cybersecurity insights, tutorials, and threat intelligence delivered to your inbox.
Discussion
Enjoyed this article?