# 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): ```sh security add-generic-password -s bw-master -a "$USER" -w ``` Wrapper pattern: ```bash 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 ```sh mkdir -p ~/scripts/-mcp && cd ~/scripts/-mcp npm init -y && npm install @ --save-exact # launcher: exec node "$HOME/scripts/-mcp/node_modules//" ``` 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 ```sh ssh-keygen -p -f ~/.ssh/ # add passphrase ssh-add --apple-use-keychain ~/.ssh/ # ~/.ssh/config: AddKeysToAgent yes + UseKeychain yes ssh-keygen -y -P "" -f ~/.ssh/ # 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 `