macos · level 3

Keychain & the security CLI

How to store and fetch secrets from the login keychain from a script.

150 XP

Keychain & the security CLI

When a script needs a secret — a GitHub PAT, an AWS access key, a database password — the lazy answer is to stick it in a dotfile or an environment variable. The correct answer on macOS is to put it in Keychain and fetch it with the security CLI. The difference matters: Keychain items are encrypted on disk, locked behind your login password, and unlockable with Touch ID.

Analogy

Keychain is a hotel room safe. You drop the passport, the spare cash, and the car-rental voucher inside, punch in a code once at the beginning of the stay, and the safe stays latched even when housekeeping is in the room. Each time you want the passport back you punch the code again — or tap your fingerprint — and the safe pops open just for you. Storing a secret in an environment variable is scrawling the hotel safe code on a Post-it stuck to the mini-bar: convenient for you, visible to every cleaner, bellhop, and crash report that wanders through. The security CLI is the small keypad on the safe — three commands to add, read, or delete, nothing fancier.

What Keychain actually is

A Keychain is an encrypted SQLite-ish database of credentials. macOS ships with several:

  • login.keychain-db — created per user, unlocked by your login password
  • System.keychain — machine-level items (Wi-Fi PSKs, EAP certificates)
  • iCloud keychain — a synced subset: Safari passwords, credit cards, shared items

For script-authored credentials, you almost always want the login keychain, and you almost always use generic passwords (not internet passwords, which are tied to a hostname/scheme).

Storing a secret

security add-generic-password -a alice -s github -w s3cret
  • -a account (any string — usually the username or team name)
  • -s service (the identifier the script will use to look it up)
  • -w value (the secret)

If the item already exists, add-generic-password errors. Pass -U to update in place:

security add-generic-password -a alice -s github -w s3cret-new -U

Better still, don't put the password in your shell history. Pipe it in:

printf 'the-real-secret' | security add-generic-password -a alice -s github -w -

-w - reads the password from stdin.

Reading a secret

security find-generic-password -a alice -s github -w

The trailing -w means "print only the password, no metadata." Without it you get the full item dump, which is useful to humans but awful inside a script.

#!/bin/bash
GITHUB_TOKEN=$(security find-generic-password -a alice -s github -w)
curl -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user

The first time a script tries to read, macOS prompts: "security wants to access the keychain — allow once / always allow?" Picking always allow makes future reads silent.

Touch ID and scripts

If the login keychain is locked (rare — it auto-unlocks at login) or the item's ACL requires explicit approval, macOS surfaces a prompt. On machines with Touch ID, that prompt is a fingerprint dialog rather than a password field. Scripts don't need to do anything special; this is a keychain-level behaviour, not an app-level one.

Why not environment variables?

  1. They show up in ps auxe until the process exits
  2. They persist in shell history if set with export FOO=bar
  3. They're copied into every child process, including ones that shouldn't see them
  4. They leak into crash reports and launchd log lines
  5. They're unencrypted on disk in dotfiles

A Keychain item is encrypted at rest, bound to a user, and requires an explicit read to unlock. Secrets that must be readable by a daemon (not tied to a user session) belong in the System keychain with a specific service ACL, or in a managed secret store — not in a plaintext launchd plist.

iCloud Keychain is different

iCloud Keychain syncs a specific set of items between your devices: Safari passwords, credit cards, shared passwords. A generic-password item added by your deploy script stays on the device it was created on. If you want it on another Mac, copy it explicitly or re-add it there. Don't assume iCloud Keychain will carry your script secrets around.

Rotating and deleting

# Delete
security delete-generic-password -a alice -s github

# Full dump of all items for a service (useful for debugging ACL problems)
security find-generic-password -s github -g
# -g "allow access" prints the password after confirming a prompt

Why this matters

Secret management on a developer laptop doesn't need Vault — you already have Keychain. Every script that needs credentials should read from Keychain, not from ~/.env.local. The cost is three lines of shell per secret; the benefit is that a stolen laptop with FileVault off still keeps its secrets locked because each Keychain item is separately encrypted at rest.