SSH 8.2 introduced support for using any U2F key in place of a private key file. Using it on macOS with full support for ssh-agent
is a bit more complex.
Generating the keys
- You must choose between
ed25519-sk
andecdsa-sk
. Tryed25519-sk
(Options 1 or 3) first. If it does not work due to device incompatibilities, fall back onecdsa-sk
(Options 2 or 4) - You must choose if you want to store the key handle as a resident key on the device. If you want to, use options 1 or 2. If not, use options 3 or 4.
A U2F attestation requires a key handle to be sent to the device. When generating the key, ssh-keygen
will create private and public key files that look similar to normal ssh key. The private key file is actually a key handle that cannot be used without the hardware token, however, the hardware token can also not be used without the key handle.
A resident key solves this problem by storing the key handle on the device. However, your key may or may not support it and only a limited number of resident keys may be stored on a device. Additionally, it may reduce the security of your ssh key as they could use it if they steal the hardware device. For this reason, a good pin is important.
It is your choice whether to use a resident key. If you do, you can load it directly to the ssh-agent using ssh-add -K
, or write the key handle and public key to disk using ssh-keygen -K
ssh-keygen -t ed25519-sk -O resident # 1
ssh-keygen -t ecdsa-sk -O resident # 2
ssh-keygen -t ed25519-sk # 3
ssh-keygen -t ecdsa-sk # 4
Updating SSH
SSH v8.2 or higher is required to use a security key. Install it with brew.
brew install openssh opensc
You can specifiy the path to the private key handle in your ssh config. Otherwise, you can configure the ssh-agent. The configuration in this guide assumes brew installed ssh and opensc into /opt/homebrew/bin/ this might be different on your system and should be verified.
ssh-agent on macOS
To be used with a security key, the ssh-agent must be on v8.2, which the system default is not.
First, disable the macOS default ssh-agent for your user.
launchctl disable user/$UID/com.openssh.ssh-agent
Next, add a new launchd service for your ssh-agent. Add the following file to ~/Library/LaunchAgents/com.zerowidth.launched.ssh_agent.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.zerowidth.launched.ssh_agent</string>
<key>ProgramArguments</key>
<array>
<string>sh</string>
<string>-c</string>
<string>/opt/homebrew/bin/ssh-agent -D -a ~/.ssh/agent -P /opt/homebrew/lib/*,/opt/homebrew/Cellar/opensc/*/lib/*.so</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
And load it with launchctl load -w ~/Library/LaunchAgents/com.zerowidth.launched.ssh_agent.plist
.
In your .bashrc
or .zshrc
, set SSH_AUTH_SOCK="~/.ssh/agent"
This plist was created using the launchd plist generator over at zerowidth. It runs the command /opt/homebrew/bin/ssh-agent -D -a ~/.ssh/agent
. -D
prevents ssh-agent from forking, and -a ~/.ssh/agent
directs the agent to create a socket file at that location that is referenced in $SSH_AUTH_SOCK
.
If you are having any issues you can always unload the launchd service and manually run the ssh-agent with the following command and read the errors/opt/homebrew/bin/ssh-agent -d -a ~/.ssh/agent -P /opt/homebrew/lib/*,/opt/homebrew/Cellar/opensc/*/lib/*.so
Storing keys in the keyring
The following stanza can be adapted and placed in ~/.ssh/config
. It removes the need to manually ssh-add
keys with nonstandard names and stores key passwords if set in the macOS keyring.
Host *
IgnoreUnknown UseKeychain
UseKeychain yes
AddKeysToAgent yes
IdentityFile ~/.ssh/id_ecdsa_sk
IdentityFile ~/.ssh/id_ed25519_sk
The first two lines direct ssh to use the macOS keychain to store passwords. The third automatically adds keys that are used to the agent and the last two specify additional keys to use. All of this can also be configured on a per host basis.
Reloading the Yubikey if it times out and fails to sign
Sometimes you might see an error where the Yubikey will not work with SSH. In order to solve this you simply have to re-add it to the agent. I wrote a simple function that works with my setup. You may need to make minor changes for it to work for you.
I added the following to my .zshrc
you can also add it to your .bashrc
function reload_ssh {
# pkcs11_path="/usr/lib/ssh-keychain.dylib"
# pkcs11_path="/usr/local/lib/opensc-pkcs11.so"
pkcs11_path="/opt/homebrew/lib/pkcs11/opensc-pkcs11.so"
ssh_add_path="ssh-add"
ssh_agent_path="ssh-agent"
$ssh_add_path -D
if [ "$1" = "-f" ]; then
echo "Forcibly killing off previous process"
launchctl unload ~/Library/LaunchAgents/com.zerowidth.launched.ssh_agent.plist
pkill -9 ssh-agent
pkill -9 ssh-pkcs11-helper
launchctl load -w ~/Library/LaunchAgents/com.zerowidth.launched.ssh_agent.plist
else
$ssh_add_path -e $pkcs11_path >> /dev/null
if [ $? -gt 0 ]; then
echo "Failed to remove previous card. Retry or run with -f option"
fi
fi
$ssh_add_path -s $pkcs11_path
$ssh_add_path
}
After adding this function you can run reload_ssh
to re-add your Yubikey and fix the most common errors.