Hardened patterns for developer workstations: Bitwarden CLI + macOS Keychain, pinned MCP servers, zero plaintext secrets. Distilled from a full red-team audit.
0
macos-credential-hardening.md
93 lines 3.4 kB View raw View code

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#

  1. Secrets live in exactly one place (a password manager). Everything else fetches at runtime and never persists. Config files hold references, not values.
  2. The vault master password never touches a file — even a chmod-600 one. macOS Keychain (security CLI) is per-app ACL'd and locks on sleep.
  3. Secret-bearing files 0600, dirs 0700. ~/.zshrc defaults to 0644 — fix it.
  4. Never export SECRET=... in a shell profile: it lands in the env of every child process, readable by any same-uid process via ps eww. Fetch per-use.
  5. Never npx -y pkg for persistent tooling — it fetches and runs the latest publish on every launch. Pin locally with a lockfile.
  6. 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, especially rm -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 `
Sign up or login to add to the discussion