kivikakk.ee

John Goerzen’s Easily Using SSH with FIDO2/U2F Hardware Security Keys came up yesterday, and I thought it was a good time to fix my mess of private keys. I already own a YubiKey 5C Nano, which sits in my laptop at all times, as well as a 5C NFC, which I figured I could use hopefully with both my phone (NFC) and tablet (USB-C) for SSH when needed.

The ideal was to drop all non-SK keys, and use move to using agent forwarding exclusively when authenticating between hosts — rarely needed, but nice for some remote-to-remote scps or git+ssh pushes. (Agent forwarding is traditionally frowned upon, since someone who has or gains access to your VPS can use your socket to get your agent to auth things, but that issue is greatly reduced when user presence is verified on each use, viz. requiring you to touch your key.)

Turns out all was pretty much that easy! Just two minor hiccups:

  • Terminus on iOS supports FIDO2 keys, no payment required (despite what some search results say; looks like it was maybe a paid-only feature during beta but since not). Non-resident Ed25519 keys work very well over NFC on iPhone, but not over USB-C on iPad. The only reference I can find is this from their “ideas” page:

     Unfortunately, iPads and iPhones with USB-C cannot be compatible with OpenSSH-generate FIDO2-based keys. Please generate new FIDO2-based keys in the Termius app. These keys are supported in OpenSSH and all Termius apps.

    Upon testing, Terminus generates a non-resident ECDSA, and that works just great. So, in the end, I have three private keys: an Ed25519 for the 5C Nano, and an Ed25519 and ECDSA for the 5C NFC for use with NFC and USB-C respectively.

  • The OpenSSH bundled in macOS (at time of writing, OpenSSH_9.9p2, LibreSSL 3.3.6) doesn’t support the use of these keys. I haven’t checked whether it’s non-resident SKs specifically or what, or whether it’s the version or just a matter of what support is compiled in.

    NixOS/nix-darwin 25.05 carries an OpenSSH_10.0p2, OpenSSL 3.4.1 11 Feb 2025, and it does!

    Using agent forwarding without losing what’s left of one’s humanity implies getting your ssh-agent setup working nicely. How?

I looked into a few different ways, but opted for the simplest: patching OpenSSH (!?). The thought process is as follows:

  • /System/Library/LaunchAgents/com.openssh.ssh-agent.plist will put an SSH_AUTH_SOCKET in your environment, which launches the system-provided ssh-agent when first addressed.
  • This is a nice, macOS-y way to do things, but we don’t want to go down any rabbit hole that involves disabling SIP, so we can’t just swap the binary path (or binary itself) out.
    • Even if we could, we still need to add launchd support to the ssh-agent in Nix. Thankfully, the needed changes are small, and easy to find; just look for __APPLE_LAUNCHD__ in ssh-agent.c.
  • In my experience, disabling the system-provided launch agent doesn’t work reliably.

We apply two patches:

  1. Add the __APPLE_LAUNCHD__ bits into the Nix-provided OpenSSH.
  2. Change SSH_AUTHSOCKET_ENV_NAME to "VYX_SSH_AUTH_SOCK".

Finally, we install our own launchd user agent (modelled upon the system one, but with our binary), which puts the socket in the VYX_SSH_AUTH_SOCK env var instead. This means we don’t need to worry about the system launch agent; it’ll only get triggered/used when something calls the system ssh binary.

Head teed!