Complete guide to setting up SSH authentication using a YubiKey's PIV module on macOS (Apple Silicon).
uv tool install yubikey-managerbrew install yubico-piv-toolChange the default PIN (default: 123456):
ykman piv access change-pinChange the default PUK (default: 12345678):
ykman piv access change-pukSet a random management key, protected by PIN (so you don't need to remember it):
ykman piv access change-management-key --generate --protectGenerate an RSA 2048 key in PIV slot 9a (Authentication):
ykman piv keys generate 9a public.pemCreate a self-signed certificate (required for the PKCS#11 module to discover the key):
ykman piv certificates generate -s "SSH key" 9a public.pemWhy a certificate? The PIV PKCS#11 module discovers keys by reading X.509 certificates from the YubiKey slots. Without a certificate, it can't see the private key. The certificate isn't used for SSH auth — it's just a wrapper for the public key.
Clean up:
rm public.pemExport your SSH public key (add this to ~/.ssh/authorized_keys on remote hosts, GitHub, etc.):
ssh-keygen -D /opt/homebrew/lib/libykcs11.dylib -e 2>/dev/null | head -1The default macOS ssh-agent doesn't allow PKCS#11 libraries. We need a custom LaunchAgent
with the -P flag to whitelist the YubiKey library.
Important:
ssh-addresolves symlinks, so the-Ppattern must match the real Cellar path, not the Homebrew symlink.
launchctl disable gui/$(id -u)/com.openssh.ssh-agentCreate ~/Library/LaunchAgents/com.custom.ssh-agent.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.custom.ssh-agent</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/ssh-agent</string>
<string>-D</string>
<string>-P</string>
<string>/opt/homebrew/Cellar/yubico-piv-tool/*/lib/libykcs11*</string>
<string>-a</string>
<string>/Users/YOUR_USERNAME/.ssh/agent.sock</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
</dict>
</plist>Replace
YOUR_USERNAMEwith your macOS username.
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.custom.ssh-agent.plistlaunchctl list | grep ssh-agent
SSH_AUTH_SOCK=~/.ssh/agent.sock ssh-add -lAdd to ~/.zshrc:
# Use custom ssh-agent with YubiKey PKCS#11 support
export SSH_AUTH_SOCK="$HOME/.ssh/agent.sock"
alias yubikey-add='ssh-add -s /opt/homebrew/lib/libykcs11.dylib'
alias yubikey-rm='ssh-add -e /opt/homebrew/lib/libykcs11.dylib'
alias yubikey-pubkey='ssh-keygen -D /opt/homebrew/lib/libykcs11.dylib -e 2>/dev/null | head -1'Some SSH clients don't read SSH_AUTH_SOCK but do read ~/.ssh/config. For those, add
IdentityAgent to point them to the custom agent socket:
Host example.com
IdentityAgent ~/.ssh/agent.sock
After login or reboot, load the YubiKey into the agent once:
yubikey-addEnter your PIV PIN when prompted. All subsequent SSH and git operations will use the agent without further prompts.
| Command | Description |
|---|---|
yubikey-add |
Load YubiKey into ssh-agent (prompts for PIN once) |
yubikey-rm |
Remove YubiKey from ssh-agent |
yubikey-pubkey |
Print your SSH public key |
ssh-add -l |
List keys currently loaded in the agent |
The -P pattern doesn't match the resolved library path. ssh-add follows symlinks, so the
pattern must match the Cellar path:
/opt/homebrew/Cellar/yubico-piv-tool/*/lib/libykcs11*
Run the agent in debug mode to see the exact reason:
ssh-agent -d -P "/opt/homebrew/Cellar/yubico-piv-tool/*/lib/libykcs11*"These come from the PKCS#11 library scanning empty PIV slots. They are harmless but noisy.
Avoid using PKCS11Provider in ~/.ssh/config globally — use the agent-based approach instead.
If ssh-keygen -D returns no keys, check that a certificate exists in the slot:
ykman piv certificates export 9a -If empty, regenerate the certificate (see Step 3).