Search your own Library folder for strings that look like API keys sometime. Application Support is full of them, sitting in plain text inside preference files and JSON blobs, written by apps that had no intention of leaking anything. It is the default place a desktop app puts configuration, and a key feels like configuration.
It is not. A key is a credential that acts on your behalf against a service that bills you, and where you put it is a design decision worth making on purpose. On macOS the answer is almost always the Keychain, and it is worth understanding both why and where it stops helping.
Why a file in Application Support is the wrong place
The problem is not that the directory is unprotected in some dramatic way. It is inside your home folder and other user accounts cannot read it. The problem is everything that legitimately touches that folder.
- Backups copy it. Time Machine, whatever cloud backup the person runs, a disk clone before a migration. Your key now exists in places nobody is thinking about, on media with different protection than the Mac it came from.
- Support flows copy it. The moment anyone says "send me your config file" or your app writes a diagnostic bundle, a plaintext key travels through email or a ticket system.
- Every process you run can read it. Any app running as you, including things you installed once and forgot, can open that file. There is no per-application boundary in a plain file.
- It survives longer than it should. Files get copied to new machines during setup migration, and keys in them outlive the reason they were created.
None of this requires an attacker. It is ordinary computer use quietly spreading a credential around.
What the Keychain actually gives you
The Keychain is a system-managed encrypted store, and on macOS it provides three things a file does not.
- Encryption tied to the login. Items are encrypted at rest and unlocked by the user's login. A copy of the keychain file on its own is not usable.
- Access control per item, keyed to code signature. An item records which applications may read it, identified by their signature rather than their path. Renaming a binary to match does not get you in, and another app reading your item triggers a system prompt rather than a silent read.
- Explicit choices about availability and sync. You decide whether an item is readable only while the Mac is unlocked, and whether it syncs through iCloud Keychain. For an API key the answer to syncing is usually no, and the default is worth setting deliberately rather than inheriting.
It also gives users a place to look. Someone who wants to know what credentials an app holds can open Keychain Access and see them, and delete one without uninstalling anything. That transparency is worth more than it sounds.
This is what our own apps do. Orbit remembers S3 access keys in the macOS Keychain, and an encrypted vault password can be remembered there too, which is why macOS may ask you to authorize Keychain access the first time. The connection profiles themselves are ordinary saved settings. The split is the point: settings in a file, secrets in the Keychain.
Environment variables are a developer convenience
Reading a key from the environment is the right call during development and for anything that runs headless. It keeps secrets out of the repository, matches how servers are configured, and makes it trivial to swap keys between runs.
It is a poor place for a shipped desktop app to keep a user's credential, for reasons that have nothing to do with encryption. Environment variables are inherited by every child process your app spawns, including ones you did not write. They get captured in crash reports and diagnostic dumps. Setting one persistently means writing it in plain text into a shell profile, which is a file in the home directory again, now also visible any time the user screen-shares a terminal. And a GUI app launched from the Dock does not inherit the shell environment at all, so the feature works when you test it from a terminal and mysteriously does not for your users.
A reasonable pattern is to read an environment variable if one is set, fall back to the Keychain, and let the app write to the Keychain when the user enters a key in settings. Developers get their workflow, users get the safe default, and nobody is editing a dotfile to use your app.
The honest limits
The Keychain raises the cost of getting a secret. It does not make it impossible, and claiming otherwise is how people end up trusting it for things it was never going to handle.
- Code running as the user is a hard case. Malware already executing in your session can put up a prompt that looks legitimate and wait for you to approve it. The Keychain protects against casual and remote access far better than against something already inside.
- Signing changes prompt again. Because access control is bound to the code signature, re-signing with a different identity makes the system treat your app as a different application, and the user gets an approval prompt for an item they already approved. This bites during development constantly and at least once in production when a certificate rolls.
- Prompts train bad habits. An app that asks for Keychain access repeatedly teaches its users to click Always Allow without reading. If you are prompting often, the bug is in how you scoped the item, not in the user.
- The safest key is the one you never hold. Where a service supports it, a short-lived token you refresh beats a long-lived key you store. A credential with a lifetime is a smaller problem than one without.
The takeaway
Treat configuration and credentials as different categories of data from the first line of code, because retrofitting that split later means migrating keys out of files you already shipped. Settings go in a file where a user can read and edit them. Secrets go in the Keychain, scoped to your app, with syncing off unless there is a reason. Environment variables stay a developer convenience. And be plain with users about what the app holds and where, because the honest version of that answer is also the one that holds up.
Filed Under
Written by
Isaac Juracich
Full-stack engineer building production software for businesses that need it done right. Based in La Crosse, WI.
More about Isaac