SSH¶
Recipes for managing FIDO2/YubiKey SSH keys and testing host configurations.
Background¶
Multiple accounts on the same hostname (e.g. two GitHub accounts) require SSH host aliases — each alias maps to a
specific key via IdentitiesOnly. Without this, SSH offers all available keys and may authenticate as the wrong
account. See ~/.ssh/config for the full configuration and comments explaining each directive.
On macOS, the built-in /usr/bin/ssh lacks FIDO2 support. These recipes use Homebrew's OpenSSH which is compiled with
libfido2. On Linux, the system SSH works natively.
New Machine Setup¶
After chezmoi init --apply, the SSH config is in place but no key stubs exist yet. For each YubiKey:
- Plug in the YubiKey
just ssh download— pulls all resident key stubs from that keyjust ssh test <host>— verify each host alias works- Swap to next YubiKey, repeat
That's it. The config lists all key files; SSH skips any that don't exist.
Adding a New Account¶
To add SSH access to a new service (e.g. a new git host or server):
- Plug in the YubiKey
just ssh generate <user>_<service> <yubikey>— creates the key- Add the
.pubkey to the service - Repeat for each YubiKey that needs access
- Add a
Hostblock to~/.ssh/config(and the chezmoi template) just ssh test <host>— verify
Naming Convention¶
Key names follow <user>_<service>_<yubikey>:
bluefing_github_nano— GitHub personal, nano YubiKeyfungible_codeberg_primary— Codeberg personal, primary YubiKeywork_github— work GitHub (single YubiKey, no suffix needed)
The application field (ssh:<name>) stored on the YubiKey determines the filename when downloaded via
just ssh download on any machine: ssh:bluefing_github_nano → id_ed25519_sk_rk_bluefing_github_nano.
Key Management¶
generate <application> <yubikey> creates a new FIDO2 resident key on the plugged-in YubiKey. Shows the full command
for review before proceeding. See just ssh show generate for detailed parameter docs.
download pulls FIDO2 resident key stubs from a plugged-in YubiKey into ~/.ssh. Each YubiKey stores its own set of
resident credentials — swap the key and run again to download from a different one. You'll be prompted for the YubiKey
PIN and may need to touch the key. Use this on new machines to get key stubs without regenerating.
The downloaded files are stubs (key handles + public key), not private keys. The actual private key never leaves the YubiKey hardware.
keys shows all FIDO2 key stubs currently in ~/.ssh.
fingerprint <key> shows the fingerprint for a specific key stub. Pass the filename or use -i to pick interactively
via fzf.
YubiKey¶
yk-info shows the plugged-in YubiKey's model, serial number, and FIDO2 resident credentials. Use this to identify
which physical key you're holding — the credential names (e.g. ssh:fungible_codeberg_primary) tell you.
Testing¶
test <host> runs ssh -T against a host alias from your config (e.g. just ssh test github-personal). GitHub
responds with the authenticated username — use this to verify the right key hits the right account. Only the currently
plugged-in YubiKey's hosts will work.
debug <host> shows verbose SSH connection output. Use this when authentication fails — it shows which key was offered,
whether ControlMaster reused a socket, and where things went wrong.
Cloning¶
Clone (and set remotes) through the host alias, never the bare hostname — the alias is what pins the right resident key and account:
git clone git@github-personal:bluefing/repo.git # ✓ personal YubiKey key, User git
git clone git@github.com:bluefing/repo.git # ✗ Permission denied (publickey)
Bare git@github.com falls through to the Host * defaults: it offers the generic
id_rsa/id_ed25519/id_ed25519_sk list (none of which are your resident stubs) and, for ssh -T, authenticates as
your login user rather than git. The alias sets User git, IdentitiesOnly yes, and the pinned IdentityFile. The
same holds for Codeberg (git@codeberg:…) and work (git@github-work:…).
Re-point an existing clone with git remote set-url origin git@github-personal:bluefing/repo.git.
The per-account remote alias is independent of the git commit identity. After cloning, set the identity too — the
dotfiles config has useConfigOnly = true, so commits are refused until you do: just git set-github-identity applies
the GitHub personal name + noreply email.
Sockets¶
ControlMaster multiplexes SSH connections to avoid repeated YubiKey auth for the same host. Sockets are per host alias
(using %n), so github-personal and github-work never share a socket despite both connecting to github.com.
sockets lists active control sockets in ~/.ssh/sockets/.
kill-socket <host> closes a multiplexed connection. Use this if a socket gets stuck or if you need to force
re-authentication (e.g. after swapping YubiKeys).
Gotchas¶
downloadwrites key stubs to~/.sshregardless of your current directory.- If
testauthenticates as the wrong user, checksocketsfirst — a stale multiplexed connection is the most likely cause. Kill it withkill-socketand retry. - macOS: if
downloadsays "Cannot download keys without provider", you're using the systemssh-keygen. Ensureopensshandlibfido2are installed via Homebrew. - SSH skips missing key stubs silently. If a YubiKey's stubs haven't been downloaded yet, those entries are just ignored.
- Clone/remote URLs must use the host alias (
git@github-personal:…), notgit@github.com:…. The bare hostname bypasses the pinned resident key and fails withPermission denied (publickey). See Cloning above. - After re-downloading a key with a new passphrase, the stale macOS keychain passphrase can no longer decrypt it
(
incorrect passphrase supplied to decrypt private key). Re-add it withjust ssh agent-add <key>(orssh-add --apple-use-keychain) to refresh the cached passphrase and load the decrypted copy into the agent.