Protecting SSH Keys with macOS Secure Enclave
265 points - today at 4:28 AM
SourceComments
YubiKeys were the only solution that fit these requirements, so I went with those. I have been using them for the last ten years or so and I've been very happy with this solution. See https://github.com/drduh/YubiKey-Guide for a description of how to make your RSA keys stored on YubiKeys work with GPG and SSH at the same time.
When I installed WireGuard on macOS, I was happy to discover that it stores private keys in the system keychain. Accessing the private key requires Touch ID.
Then one day after a system upgrade, the WireGuard app crashed on startup with:
[APP] Unable to open config from keychain: -25300
Then the app deleted all profiles because they are all "orphaned", apparently for "keychain consistency" [0].From then on, I just use a text editor to edit my WireGuard configuration files, and import them to the app. The bug still exists to this day [1].
[0] https://github.com/WireGuard/wireguard-apple/commit/dfb685f2...
[1] https://www.reddit.com/r/WireGuard/comments/wyt828/osx_wireg...
It is not one of the cryptographically safe curves, likely intentionally so.
This is why I'm skeptical that the approach can scale reliably, even though I'm relying on it today. (Yes, PATs would fix this, but the enclave model is what I'm trying to retain for now.) What I've done instead is wrote a small daemon that sets the SSH authentication socket and does agent forwarding. This works when I'm traveling with laptop...but not yet from my mobile phone.
Example:
Laptop Remote Mac
βββββββββββββββββββββββ ββββββββββββββββββββββββββββ
β Secretive β β ssh / git commit -S β
β (Secure Enclave) β β β β
β β² β ForwardAgent β βΌ β
β βββ ssh ββββββΌββββββββββββββββββΊβ ~/.ssh/agent.sock β
β β β (stable proxy) β
βββββββββββββββββββββββ β β β β
β βΌ βΌ β
β forwarded local β
β laptop Secretive β
β agent (if you sit β
β at this Mac)β
ββββββββββββββββββββββββββββ
NB: I'm not using Rex instead of herdr and because Rex uses its own protocol, I always need to have one SSH tunnel to reset the remote agent socket. Not optimal, but it works. All of this feels wrong from a UX perspective, and it feels like I'm holding on too hard on secure enclave for what my use cases are. Feedback appreciated!SSH keys in plaintext was one of the first thing that bagel [0] our developer endpoint secret detection focused on. Then we embedded bagel as a library in our developer endpoint protection [1].
[0] https://boostsecurityio.github.io/bagel/ [1] https://boostsecurityio.github.io/dazio/docs/install/
I found I was getting fingerprint fatigue. Do I need a fingerprint request every time my dev server wants to connect to dev Supabase? No. That's expected, and zero risk. There's no data there.
I worried that a nefarious request would be auto-approved by me. When you see 20 requests a day, your muscle memory just reaches for the sensor.
So I have a separate 1Password vault that a service account [0] can read. That token is in my environment as OP_SERVICE_ACCOUNT_TOKEN, which the `op` tool understands.
It also has access to a vault with 'AI-safe' keys that I want Claude to be able to access without asking me.
Now, these requests work invisibly. And when I see a fingerprint popup, it's unusual, and I don't auto-approve by habit.
I also keep an item called '_One-day keys' in a separate vault. (The underscore just pushing it to the top of the list for easy access.)
It has prod keys, mostly restricted read-only. If required, I move that over to the open vault. Claude knows this item and has instructions to remind me to move it back out when it's finished what it was doing.
The author says he doesnβt like the idea of having keys sitting on his file system ready to be snatched by bad actors.
Thatβs why they should be passphrase protected.
You use ssh agent to store the keys in memory and for convenience you can store the passphrases in keychain. MacOS OpenSSH has the UseKeychain config directive for that.
I use Yubikey/FIDO2 myself, but not because private keys cannot be protected on a file system.
It's effectively an open source version of a YubiKey, the hardware itself is all open source and it handles ed255519 SSH keys really well.
inb4 they're not the same, I know, I really like what the TKey team is doing.
> https://news.ycombinator.com/item?id=46025721
One of the problems I constantly face is that the SSH configuration is not portable when I use these esoteric things. Even with Secretive (which plugs a simple SSH socket), remote agent forwarding sometimes fail. (ie. Due to the old version on the server or configuration issue, or something else setting SSH_AUTH_SOCK...)
Moreover, it is not possible to drop these files into a file-sharing folder, like OneDrive or iCloudDrive or SMB share, and share them with many hosts at the same time. (Especially when different operating systems are involved...)
I found the hacker way simplest and easiest; put a basic Makefile with bunch of rules, have a "central" host/machine to distribute the keys and configuration from that point on...
But I think I would take this over a yubikey. I really disliked the fact that one usb port was constantly in use.
On iOS/ipad/(also macOS) you can also use the SSH terminal emulator Termius which has support for SE keys built in as well. So I have the set of keys synced to my VPS, this way I know that only my physical devices can log in and nothing else.
It's often the case that we make much effort into securing the key but expose it via an open agent socket at a well known path or use an open control master path for multiplexing.
Certain automation and dev tools like Ansible make it a pain to work if agent key requires a confirmation.
No more passwords to enter anywhere, and whatever an (AI) agent or malware is trying to do will require a human interaction at least, and not work when the screen is locked. The public keys authorized for this are distributed to the fleet via puppet, so we get control over who gets to sudo permissions and can easily provision or decommission machines.
Pretty stoked about it so far.
I think this is incorrect and the "ssh:" at the end is indeed a comment.
Work on macOS 26 and later.