Making macOS Stop Asking for Keychain Access on Every Connect: A Keychain Choice
A Shellby postmortem: the same password-storing code popped a permission dialog on every connect on macOS — switching keychains fixed it.
Shellby stores server passwords and private keys in the system keychain. During development I hit an annoying problem: on macOS, every connect popped up a “Shellby wants to use confidential information stored in your keychain” dialog that you had to click through. Not on iOS — just macOS.
This post is about where that dialog comes from, and how switching keychains cures it root and branch — a small “same code, different storage backend” choice, but behind it are the differences between macOS’s two keychain models.
Where the dialog comes from
macOS actually has two keychains:
One is the traditional file-based keychain (the login.keychain family). Its access authorization is decided by the binary’s code-signing ACL — the first time an app accesses a keychain item, you authorize “always allow,” and the system records that app’s code-signing info into the item’s ACL. Later, the same app comes to access it, and if the signature matches, it’s allowed.
That’s exactly where the problem is. During development you recompile constantly with an ad-hoc signature, and every recompile changes the binary’s cdhash (code-signing digest). Once the cdhash changes, the “trusted app” recorded in the ACL no longer matches — the system thinks a “different” program is accessing it, so it pops the dialog again. Change a line, rerun, connect — dialog. Change another line, rerun, connect — dialog again.
Switch to the data-protection keychain
The fix isn’t to tame the traditional keychain’s ACL; it’s to switch outright to a different one: the data-protection keychain — the one iOS uses.
The change is tiny — every keychain query carries one flag:
query[kSecUseDataProtectionKeychain as String] = true
The data-protection keychain’s authorization model is completely different: it doesn’t look at the binary’s code-signing ACL, it looks at the app’s keychain-access-groups entitlement — deciding by “does this App ID / team have the right to access this access group.” And the App ID and team are stable, they don’t change when you recompile. So no matter how the cdhash changes, as long as it’s still the same App ID signed by the same team, it’s allowed all the way through, and the dialog is gone.
The cost is a prerequisite: the app has to carry the keychain-access-groups entitlement and be signed with a stable team. So this path is “trade a bit of config for killing an interaction noise” — declare the access group in the entitlements, set up the development team in the project, and the dialog is gone.
The migration trap from switching keychains
There’s a trap that has to be spelled out: the traditional keychain and the data-protection keychain are two independent stores.
After switching, the data-protection keychain is empty — the passwords and imported private keys you previously wrote into the traditional keychain aren’t found in the new one. So users upgrading to this version have to re-enter their saved host passwords and private keys once. This isn’t a bug; it’s the inevitable result of two physically separate stores, and it can only be explained in the upgrade notes.
An aside — a naming legacy: Shellby was originally called mShell, and after the rename, the keychain’s service name still uses the old com.mshell.secrets — deliberately kept. Because changing the service name is equivalent to changing the storage location again, so all the user’s existing credentials become unreadable, forcing a needless second re-entry. A rename is a product matter; the storage key is a data matter; the two can’t move together.
It solves sync as a bonus
Choosing the data-protection keychain has a bonus that dovetails with multi-device sync. The data-protection keychain natively supports iCloud Keychain’s end-to-end sync — add kSecAttrSynchronizable = true to a keychain item and it syncs across devices, unreadable even to Apple (end-to-end encrypted).
Local and syncable keys even have different accessibility: local keys use …AfterFirstUnlockThisDeviceOnly (strictly locked to this device), while syncable keys must relax to …AfterFirstUnlock (because synchronizable doesn’t allow ThisDeviceOnly). Lookups and deletes both use kSecAttrSynchronizableAny, so they hit both across the sync toggle and before/after migration. The traditional file-based keychain is far clumsier at all of this — after switching, “kill the macOS dialog” and “support end-to-end sync” are two things solved by one choice.
Takeaways
The trap itself is small, but it’s a textbook example:
- The same “store a password” need has two implementations on macOS, with completely different authorization models. The default one (traditional ACL) gets repeatedly triggered by cdhash changes during development;
- Switching to the data-protection keychain (authorized by App ID / access group, not by code-signing ACL) cures the dialog, at the cost of declaring an entitlement and signing with a stable team;
- Switching storage means accepting migration cost: the two keychains are physically separate, old data is unreadable after switching, users must re-enter — and don’t casually change the service name and force a second re-entry;
- Bonus: the data-protection keychain natively supports iCloud end-to-end sync, so one choice incidentally solves cross-device credential sync.
Some “UX problems” are rooted not in your code logic but in which of the platform’s underlying mechanisms you chose. The dialog wasn’t a bug you wrote — it was the wrong keychain you picked.
Comments