Recently, I decided to give FIDO2-backed ssh keys a go for work and personal use. The theoretical benefits of keeping your private keys secure and irretrevable are pretty compelling in certain use cases.
Background on the Issues with Yubikeys and SSH on MacOS
With modern versions of OpenSSH (8.3+), you can use SSH keys stored in a modern Yubikey that supports FIDO2 (specifically FIDO 2.1 for credProtect, which early versions of Yubikey 5 did not support). This allows you to use the key in an attached Yubikey to authenticate to remote SSH servers, including Linux, GitHub and anything that supports ed25519-sk and ecdsa-sk keytypes (essentially ed25519 and ecdsa keys in a Yubikey or other hardware device). For their own reasons, Apple has the MacOS bundled versions of OpenSSH (including ssh-keygen and ssh-agent) built with support for this disabled (including as of November 2024 with MacOS Sequoia 15.1.1). We can get around this by installing the clients via Homebrew.
There are caveats, though. If you’re using regular SSH keys that have their private keys encrypted on disk, you cannot use the Homebrew ssh-add
to retrieve private key passwords stored in the MacOS keychain, which is convenient for having your terminal sessions automatically load the keys. Also, you need to work around apple’s launchd-backed ssh-agent, which will automatically spin up if you try and use the default socket that MacOS passess to new interactive shells. This agent will not support having FIDO2 backed keys loaded into it. I’ve come up with a way around these limitations that suffices for me (if you want, you could modify launchd to just spin out the vanilla version, but my work computer is too locked down for that). The reasons for the vanilla OpenSSH not nativly supporting the MacOS keychain are mostly aparently mostly political.
Installing Reqired software
You’ll need to install several pieces of software:
- Homebrew: a package manager that the majority of people use to install Open Source software on MacOS these days
- OpenSSH (via Homebrew): for the client tools
- ssh-askpass: a third party utility for ssh-agent to send prompts to the user, specifically to touch or to enter a PIN
- ykman: yubikey utility to set a PIN and manage your keys on the yubikey.
First, if you don’t already have it, install Homebrew:
|
|
Then, install the other requirements (run a brew update
if brew was already installed):
|
|
Configuration
After doing the above, type exec $SHELL
so that you can be sure you’ve loaded new homebrew PATH and then run which ssh
. You should get something like:
|
|
If it’s outputing /usr/bin/ssh
then you’ve got a problem with your shell’s PATH.
You’re then going to need to add the following to your ~/.zshrc
(or other shell’s startup file):
|
|
So what’s going on here? Well as I described above, we have a bit of a chicken and egg problem in that the vanilla OpenSSH ssh-add doesn’t support the apple keychain (which we want to be able to load password-protected private keys automatically) and the apple-bundled ssh-keygen/ssh-agent/ssh do not support FIDO2 backed keys. Here’s what the above code is doing and why:
- Unsetting SSH_AUTH_SOCK - This is so that ssh-add doesn’t reach out to the launchd-backed ssh-agent socket, which will then automatically launch the MacOS bundled ssh-agent that doesn’t support yubikeys
- The script then checks if this is a MacOS system (I have a unified .zshrc that I use on non-macos machines as well, including servers)
- If it’s MacOS (Darwin), we check if we have a file that we’ll otherwise create at
$HOME/.ssh/environment-$SHORT_HOST
that we’ll use for sebsequent shells to source from and not need to spin up their own ssh-agents, meaning you’ll have one agent running at a time that will be shared by any subsequent terminal windows. - If the file is present, we’ll source it and check to see if the socket is active. If it is and there’s no keys, we load our keys with the MacOS bundled ssh-add that will load the keys with passphrases from the Macos Keychain
- If the socket isn’t active, we’ll spin up the homebrew ssh-agent and update the file with the new agent’s socket so that new shells can use it. We’ll also load the key’s with the MacOS bundled ssh-add
- You’ll also note that
SSH_ASKPASS
andDISPLAY
are set. This is to pop up a prompt in a new GUI window to prompt us for our yubikey PIN (As of writing, I’ve not reliably gotten the agent to prompt via the ssh client on the CLI)
Any subsequent ssh CLI uses will now be via the vanilla OpenSSH clients. We now only use the bundled MacOS ssh-add to initially load the keys when the shell starts up and initialises ssh-agent. (You can still load them via the vanilla OpenSSH clients, but you’ll be prompted for your passphrase). You can load the keys off of any connected yubikey into the agent with ssh-add -K
.
Setup the yubikey
First, if you haven’t done so already, you may want to setup a PIN for the FIDO function (you should also set one for the PIV and GPG functions as well). There is no default PIN for FIDO. You will need a PIN for FIDO if you want to do full verify-required
rules via an sshd_config option. Entering your PIN wrong 8 times will lock/wipe the yubikey. Note that setting a PIN will also prompt for it during web browser FIDO auth requests. If you don’t want a PIN prompt for SSH/web requests and don’t need or want verify-required
, skip this step.
Note that the PIN here can be any standard alphanumeric password. It DOES NOT need to be just numbers and should be a relativly complex set of alphanumeric characters:
|
|
Now we can create some ssh keys to use. In general, you want to use ed25519
keys over ecdsa
.
You have several different options:
Siutation | Description | Command Example |
---|---|---|
No PIN or touch | Your yubikey will need to be inserted, but that's it. Good for things that use SSH a lot over the course of a day (eg git) or are lower security, like a dev environment. | ssh-keygen -t ed25519-sk -O resident -O no-touch-required -O application=ssh:dev_environment_key -C "Low risk dev key" |
PIN required but no touch | Entering the PIN will be required but touching the physical key will not. | ssh-keygen -t ed25519-sk -O resident -O verify-required -O no-touch-required -O application=ssh:prod_key -C "Production key" |
No PIN but touch is required | You will only need to touch the YubiKey to authenticate. | ssh-keygen -t ed25519-sk -O resident -O application=ssh:touchonly_key -C "Touch Only Key" |
A PIN and a touch are both required | Both the PIN and touching will be required. Probably overkill. | ssh-keygen -t ed25519-sk -O resident -O verify-required -O application=ssh:pentagon_key -C "Pentagon Key" |
Keep in mind, sshd can be configured to have verify-required
set, meaning for those situations you’ll need to have a key created with that.
You’ll notice that the resident
option is set for all examples. This stores the key handle on the yubico itself that can be implicitly referenced by clients or downloaded with ssh-keygen -K
. If resident
is not set, the key handle is saved locally and not on the yubikey and will manually need to be synced to any other device that wants to use the key. Not setting this is probably overkill except for explicit, highly secure situations where you will only use the key from one device ever (essentially meaning you need the yubikey AND the handle file).
-C "Production key"
is for the comment in the public key that should make it easier to match to the -O application=ssh:prod_key
which will be the unique name in the Yubikey that you can see with ykman
. If you’re creating more than one key, use -f
like -f .ssh/id_ed25519_sk_prod
as ssh-keygen will create the public and key handle files as /.ssh/id_ed25519_sk.pub
and ~/.ssh/id_ed25519_sk
respectivly.
So Let’s generate a full touch and PIN required key:
|
|
You now have an ssh key on your yubikey. I had to enter the PIN as well as touch the yubikey to create it. I now have a handle written to .ssh/id_ed25519_sk
and the public key written to .ssh/id_ed25519_sk.pub
. The public key here can be written to anywhere else you’d use a public SSH key (that has to support the SK keytypes). So places like github or remote hosts in ~/.ssh/authorized_keys
. The handle is only needed if you’re not using ssh-agent at all. It can otherwise be deleted.
You can also see your keys and other FIDO credentials on the yubikey:
|
|
If you decide you no longer want the key, run ykman fido credentials delete 75118dbf
and it will prompt you for your PIN and verify.
Now let’s use the key. We will need to manually load the key into the agent if not with the vanilla openssh:
|
|
From there, you should be able to ssh and a GUI will pop up for the PIN. Note that the ssh connection will then only procede after you touch the yubikey. It only prompts once. If you want to force ssh to use the key, you can either specify it with ssh -i .ssh/id_ed25519_sk
(which is the handle) or adding to ~/.ssh/config
the option IdentityFile ~/.ssh/id_ed25519_sk
under any host you want.