How I Use a YubiKey FIDO2 Security Key for SSH on Linux

AI-generated

SSH private keys are convenient, but they are still files which can be copied. I use a YubiKey FIDO2 security key for my normal SSH access, so the actual signing operation needs the physical key and a touch.

On my Arch and Gentoo workstations I use SSH for Git hosting, servers in my homelab and some deployment systems. A normal ed25519 SSH key is already much better than passwords, but it is still a reusable secret. If somebody gets the private key and its passphrase, they can use it from another machine.

FIDO2 SSH keys work differently. The private key material stays on the YubiKey. OpenSSH stores a small key handle in ~/.ssh, but that file alone cannot authenticate anywhere. For each SSH login, the YubiKey needs to be plugged in and touched. With user verification enabled, it also asks for the YubiKey PIN.

This is not a replacement for good SSH server configuration, but it removes one quite common problem: accidentally copying a private key into a backup, a VM image or the wrong ~/.ssh folder.

Requirements

I am using a YubiKey with FIDO2 support and a reasonably recent OpenSSH version. Most current Linux distributions include this by default. OpenSSH calls these key types ed25519-sk and ecdsa-sk, where sk means security key.

I prefer ed25519-sk. It is smaller and modern, and current OpenSSH versions support it well.

First I check if my local OpenSSH binary knows the security key key types:

$ ssh -Q key | grep sk
sk-ecdsa-sha2-nistp256@openssh.com
sk-ssh-ed25519@openssh.com

The YubiKey needs a FIDO2 PIN. You can configure it with ykman or the graphical YubiKey Manager. The command line tool is available as ykman on most distributions.

$ ykman fido access change-pin
Enter your existing PIN:
Enter your new PIN:
Repeat for confirmation:

Do not confuse the FIDO2 PIN with the YubiKey PIV PIN, OpenPGP PIN or an OTP configuration. SSH FIDO2 keys use the FIDO application on the key.

Creating my SSH security key

I create the key with ssh-keygen. The -O verify-required option tells OpenSSH that the key requires user verification, which means the FIDO2 PIN is needed in addition to touching the key.

$ ssh-keygen -t ed25519-sk -O verify-required -f ~/.ssh/id_ed25519_sk -C "yves@yubikey"
Generating public/private ed25519-sk key pair.
You may need to touch your authenticator to authorize key generation.
Enter PIN for authenticator:
You may need to touch your authenticator again to authorize key generation.
Your identification has been saved in /home/yves/.ssh/id_ed25519_sk
Your public key has been saved in /home/yves/.ssh/id_ed25519_sk.pub

The resulting private key file is not a normal private key in the old sense. It contains the public key and information required to ask the YubiKey for a signature. It is still worth protecting with the normal file permissions.

$ ls -l ~/.ssh/id_ed25519_sk*
-rw------- 1 yves users  582 Oct 30 12:00 /home/yves/.ssh/id_ed25519_sk
-rw-r--r-- 1 yves users  108 Oct 30 12:00 /home/yves/.ssh/id_ed25519_sk.pub

I use a separate FIDO2 key per workstation or purpose. This makes it easier to revoke a lost laptop or key later without touching every other system.

The generated key is normally non-resident. This is what I want for SSH. The YubiKey does not need to list the credential itself, and the small id_ed25519_sk reference file is required on the workstation.

Adding it to Git hosting

For GitHub, GitLab, Gitea or a self-hosted Git server, I copy the public key as usual.

$ cat ~/.ssh/id_ed25519_sk.pub
sk-ssh-ed25519@openssh.com AAAAG... yves@yubikey

I paste this complete line into the SSH key section of the Git hosting account. There is nothing special to configure on the Git server as long as it supports modern OpenSSH public keys.

For GitHub I can test the connection like this:

$ ssh -T git@github.com
Confirm user presence for key ED25519-SK SHA256:...
Hi yves! You've successfully authenticated, but GitHub does not provide shell access.

I normally add an explicit SSH configuration entry, especially when I have several Git identities.

Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_sk
    IdentitiesOnly yes

IdentitiesOnly yes avoids SSH trying every random key loaded in my agent. This is useful when a Git host has a low limit for failed key attempts.

The same configuration works for private Git servers. For normal server access I use an entry with the matching host name and IdentityFile.

SSH agent behaviour

The SSH agent can use FIDO2 keys too. I add the key after logging in:

$ ssh-add ~/.ssh/id_ed25519_sk
Enter PIN for authenticator:
You may need to touch your authenticator to authorize key addition.

$ ssh-add -l
256 SHA256:... yves@yubikey (ED25519-SK)

The agent does not get a copy of the key stored on the YubiKey. It only remembers the local key reference. When I authenticate later, the YubiKey still needs to be present and I still need to touch it.

This is a nice difference to a normal SSH agent key, which can sign requests until it is removed or the agent exits.

$ ssh-add -D
All identities removed.

I do not forward my SSH agent to servers unless I really need it. FIDO2 user presence helps, but an agent forwarded to an untrusted system is still not something I want to leave around. For my KVM and Proxmox systems I prefer connecting from my own workstation directly. My GPU pass-through setup already has enough moving parts without forwarding credentials through VMs, as I wrote in Linux KVM GPU pass-through with identical graphic cards.

Keeping a recovery key offline

A hardware key can be lost, damaged or simply left at home. I always create a recovery SSH key before relying on the FIDO2 key everywhere.

This recovery key is a regular encrypted ed25519 key. The important part is that I do not keep it in my normal workstation ~/.ssh folder and I do not add it to my SSH agent.

$ ssh-keygen -t ed25519 -a 100 -f ~/recovery/id_ed25519_recovery -C "yves@ssh-recovery"
Generating public/private ed25519 key pair.
Enter passphrase for "/home/yves/recovery/id_ed25519_recovery":

I add its public key to Git hosting and important servers, then store the private key on encrypted offline storage. I also keep the passphrase separately. The recovery key should be tested once, otherwise it is not a recovery plan.

A recovery key is still a reusable private key. I treat it as an emergency credential, not as my daily SSH identity.

If I lose a YubiKey, I remove its public key from Git hosting and from authorized_keys on my servers. Then I create a new FIDO2 key with the replacement YubiKey.

For systems where I also use Secure Boot, I keep the recovery material separate from my Secure Boot keys. My Arch Linux Secure Boot with systemd-boot, sbctl and a Unified Kernel Image setup has different keys and a different recovery procedure.

Using a YubiKey for SSH is a small change in the daily workflow. Plug it in, enter the PIN, touch it. For the reduced risk of copied SSH private keys, it is worth the extra step imho.