ssh authentication via Yubikeys

Note: In an older version of this article, some of the things stated here were wrong. Sorry for that.

I recently played with my Yubikey to establish them as second factor for my ssh keys. The process is straight-forward, however it took me some time to go through Yubico’s documentation. Here I write the process down in my own words.

ssh public key authentication can be hardened to require a hardware token like the Yubikeys (series 5 onwards). My Yubikey 4 is not supported, it’s too old - FIDO2 is required. From OpenSSH 8.2 and 8.3 respectively (for resident keys) onwards, yubikeys are supported. I do only require them for Linux, although the documentaton mentions some not-officially-supported-but-should-work methods to get it also working on Windows and MacOS.

Discoverable (resident) and non-discoverable (non-resident) keys

Before we start, there are two different keys: Discoverable (or resident) and non-discoverable (or non-resident) keys. Resident keys are stored on the Yubikey, while non-resident key use the Yubikey’s master key, but do not store anything on it. For ssh, non-resident keys should be preferred because of two reasons: First, they do not consume any of the sparse certificate slot on the Yubikey. Second, if the Yubikey gets lost or stolen, an attacker cannot possibly obtain the ssh private key based on the Yubikey alone.

In the case of resident keys, the Yubikey uses PIV slots to store the ssh keys (RSA or EC). The Yubikey 5 has 25 slots available, but they are shared with other PIV certificates, so be careful to not waste them. Firstyear wrote this might become a problem, because for every passkey you will need one of those slots. Therefore again, non-resident keys should be preferred.

TL;DR

Tested on my openSUSE Tumbleweed laptop against a Leap 15.5 and a MicroOS server

Non-Discoverable (non-resident) key:    Require additional identity file, do not occupy storage on Yubikey
Discoverable (resident) key:            Can in principle be used anywhere, occupy one slot on the Yubikey per ssh key

Recommended: non-resident keys
  1. Insert your Yubikey (Firmware 5.2.3 or higher)
  2. Set a PIN for FIDO2 on the Yubikey
  3. Generate the key pairs:

Non-discoverable keys (recommended, more secure, cannot be used without the credentials id file, do not occupy slot on Yubikey):

$ ssh-keygen -t ed25519-sk

Discoverable/resident keys (will occupy one of the sparse credential slots on the Yubikey, not recommended)

$ ssh-keygen -t ed25519-sk -O resident -O application=ssh:my-hostname -O verify-required
  1. Transfer the public key to the remote host REMOTE
$ ssh-copy-id -i ~/.ssh/id_ed25519_sk REMOTE
  1. Check if the login works by enforcing the new identity. ssh should ask you for your PIN:
$ ssh -i ~/.ssh/id_ed25519_sk -o 'IdentitiesOnly yes' REMOTE

And that’s it!

Step-by-step guide

Remote system configuration

this was not needed in my case. I leave it here for you, if you need it. See the official documentation for more information.

OpenSSH supports FIDO2 authentication by default since somewhere in OpenSSH 8, but it might not check for the user verification via PIN. In case you run into trouble, add the following to your sshd_config on the OpenSSH server end:

PubkeyAuthOptions verify-required

On the client side, you can also append verify-required to an authorized_keys entry:

sk-ssh-ed25519@openssh.com ENTRY verify-required

I did not need to do those changes on openSUSE Leap 15.5 and MicroOS.

  1. Insert your Yubikey (Firmware 5.2.3 or higher)
  2. You probably need to set a PIN for the Yubikey, otherwise it doesn’t work
  3. Generate a new key using ed25519 (or ecdsa if you really want. Replace my-hostname with your identifier

Note: Prefer ed25519 over ecdsa because of concerns over the trustworthiness of NIST produced curves used in ECDSA.

ssh-keygen -t ed25519-sk

A key pair, id_ed25519_sk and id_ed25519_sk.pub is generated. The private key is not the actual private key, but part of the key generation process. This secret is used in conjunction with the Yubikey to generate the private key. This makes it more secure than resident keys, because you always need this identity key AND the Yubikey. The public key id_ed25519_sk.pub is the actual public key. You can (and should) add an additional passphrase to further harden the ssh key.

Use ssh-copy-id to copy the public key to a remote host, e.g. REMOTE (the -i ... option is optional):

$ ssh-copy-id -i ~/.ssh/id_ed25519_sk REMOTE

And then you can check the key login via the following - ssh should ask for your PIN

$ ssh -i ~/.ssh/id_ed25519_sk -o 'IdentitiesOnly yes' REMOTE

Create discoverable keys

  1. Insert your Yubikey (Firmware 5.2.3 or higher)
  2. You might need to set a PIN for the Yubikey for FIDO2, otherwise it’s refused or not secure. Do it.
  3. Generate a new key using ed25519 (or ecdsa if you really really want). Replace my-hostname with your identifier
$ ssh-keygen -t ed25519-sk -O resident -O application=ssh:my-hostname -O verify-required

This generates a new keypair ~/.ssh/id_ed25519_sk and ~/.ssh/id_ed25519_sk.pub. Note that the private key is mostly just a reference to the secret stored on the Yubikey, although it does not appear that way. The public key id_ed25519_sk.pub is the actual public key.

From here, the rest is the same as above. Copy the key using ssh-copy-id and then test it using the above stated arguments for ssh.

List and delete keys (resident keys only)

Reminder: For ssh, non-resident keys should be preferred to save the sparse credential slots on the Yubikey. See Firstyear’s blog post on why this is an issue.

The Yubikey uses it’s PIV module to store RSA and EC keys used for ssh. Mine (Yubikey 5) has 25 slots available. Per ssh key one slot is occupied and you might need those slots also for other applications. If you want to really use passkeys, then you can only store 25 passkeys on a Yubikey, so don’t be wasteful. Use non-resident keys for ssh.

However, in case you still need resident keys, you can list the currently used certificates via

ykman fido credentials list

And to delete a certain credential the delete subcommand with the corresponding key id, e.g. for da7fdc

ykman fido credentials delete da7fdc

Cave: The private key does not get deleted unless it is overwritten.

Outlook

Once I figured the difference between discoverable and non-discoverable keys and how they interact with ssh, it’s pretty much straight-forward to use a Yubikey to secure your ssh keys. I have this now setup for my primary Yubikey and one device and will need to do the same for my secondary computer (I use non-discoverable keys).

I think that Yubikey-based ssh authentication can improve the security. But before I’m comfortable to limit ssh authentication to this method and remove my old keys, I need a secondary Yubikey. I currently have a older Yubikey 4, which is not supported. And I do not feel comfortable to have only one Yubikey that opens important ssh connections. It’s a single point of failure (or loss), that would lock me out otherwise.

Also I noticed that it’s less convenient because I need to insert the Yubikey PIN for every ssh connection. ssh-add allows to unlock the key passphrase, but still for every connection I get a PIN prompt. Perhaps there is a way to avoid this and unlock the Yubikey for a certain time, but that’s something that I still need to figure out.

For now this works pretty well, and I look forward to play a bit more with my Yubikeys.

Minus points for Azure

And 100 minus points to Azure, who still do not support ed25519 keys. Sorry, this annoys me that much, that I just had to rant about it.