Hardened patterns for developer workstations: Bitwarden CLI + macOS Keychain, pinned MCP servers, zero plaintext secrets. Distilled from a full red-team audit.
0
Secure Local Credential Infrastructure (macOS)#
Hardened patterns for developer workstations: Bitwarden CLI + macOS Keychain, pinned MCP servers, zero plaintext secrets. Distilled from a full red-team audit.
Principles#
- Secrets live in exactly one place (a password manager). Everything else fetches at runtime and never persists. Config files hold references, not values.
- The vault master password never touches a file — even a chmod-600 one.
macOS Keychain (
securityCLI) is per-app ACL'd and locks on sleep. - Secret-bearing files 0600, dirs 0700.
~/.zshrcdefaults to 0644 — fix it. - Never
export SECRET=...in a shell profile: it lands in the env of every child process, readable by any same-uid process viaps eww. Fetch per-use. - Never
npx -y pkgfor persistent tooling — it fetches and runs the latest publish on every launch. Pin locally with a lockfile. - SSH keys get passphrases, stored in Keychain via
ssh-add --apple-use-keychain.
Bitwarden CLI unattended unlock#
Three tiers, three storage classes:
| What | Where |
|---|---|
| BW_CLIENTID / BW_CLIENTSECRET | ~/.config/bw/env (0600) |
| BW_PASSWORD (master) | Keychain item bw-master |
| API keys | vault items, fetched per session |
One-time, interactive (user types the master password; never scripted):
security add-generic-password -s bw-master -a "$USER" -w
Wrapper pattern:
set -euo pipefail
[ -z "${BW_CLIENTID:-}" ] && [ -f "$HOME/.config/bw/env" ] && source "$HOME/.config/bw/env"
if [ -z "${BW_PASSWORD:-}" ]; then
BW_PASSWORD=$(security find-generic-password -s bw-master -w 2>/dev/null || true)
fi
export BW_PASSWORD
BW_SESSION=$(bw unlock --passwordenv BW_PASSWORD --raw)
# --passwordenv keeps the password out of ps output
Pinned MCP/tool installs#
mkdir -p ~/scripts/<name>-mcp && cd ~/scripts/<name>-mcp
npm init -y && npm install <pkg>@<exact-version> --save-exact
# launcher: exec node "$HOME/scripts/<name>-mcp/node_modules/<pkg>/<entry>"
Upgrades become explicit: bump, install, review diff, restart.
Shell hygiene#
chmod 600 ~/.zshrc ~/.zsh_history- Document secrets as comments with fetch commands, never values
setopt HIST_IGNORE_SPACE; prefix sensitive commands with a space- A secret that ever appeared in a profile/history is burned: rotate, purge, and check agent/tool session logs for copies
SSH#
ssh-keygen -p -f ~/.ssh/<key> # add passphrase
ssh-add --apple-use-keychain ~/.ssh/<key>
# ~/.ssh/config: AddKeysToAgent yes + UseKeychain yes
ssh-keygen -y -P "" -f ~/.ssh/<key> # MUST FAIL after
Shell script input handling#
- Constrain names:
[[ "$NAME" =~ ^[a-z0-9][a-z0-9-]{0,62}$ ]] - Escape sed substitutions:
esc() { printf '%s' "$1" | sed 's/[&|\\]/\\&/g'; } set -euo pipefail; quote every expansion, especiallyrm -rf "$DIR"
API key blast radius (registrars, DNS, etc.)#
- Scope keys to named resources (domains), never account-wide
- Set spend caps; disable register/transfer permission if offered
- Record scope in the vault item notes; re-verify quarterly
Accepted residuals#
- stdio MCP server env is same-uid readable while running — mitigate by scoping keys, not elimination
- Agent session DBs accumulate pasted content — reference secret locations, never values, in agent sessions `