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 rendered
1# Secure Local Credential Infrastructure (macOS) 2 3Hardened patterns for developer workstations: Bitwarden CLI + macOS Keychain, 4pinned MCP servers, zero plaintext secrets. Distilled from a full red-team audit. 5 6## Principles 7 81. Secrets live in exactly one place (a password manager). Everything else 9 fetches at runtime and never persists. Config files hold references, not values. 102. The vault master password never touches a file — even a chmod-600 one. 11 macOS Keychain (`security` CLI) is per-app ACL'd and locks on sleep. 123. Secret-bearing files 0600, dirs 0700. `~/.zshrc` defaults to 0644 — fix it. 134. Never `export SECRET=...` in a shell profile: it lands in the env of every 14 child process, readable by any same-uid process via `ps eww`. Fetch per-use. 155. Never `npx -y pkg` for persistent tooling — it fetches and runs the latest 16 publish on every launch. Pin locally with a lockfile. 176. SSH keys get passphrases, stored in Keychain via `ssh-add --apple-use-keychain`. 18 19## Bitwarden CLI unattended unlock 20 21Three tiers, three storage classes: 22 23| What | Where | 24|------|-------| 25| BW_CLIENTID / BW_CLIENTSECRET | `~/.config/bw/env` (0600) | 26| BW_PASSWORD (master) | Keychain item `bw-master` | 27| API keys | vault items, fetched per session | 28 29One-time, interactive (user types the master password; never scripted): 30 31```sh 32security add-generic-password -s bw-master -a "$USER" -w 33``` 34 35Wrapper pattern: 36 37```bash 38set -euo pipefail 39[ -z "${BW_CLIENTID:-}" ] && [ -f "$HOME/.config/bw/env" ] && source "$HOME/.config/bw/env" 40if [ -z "${BW_PASSWORD:-}" ]; then 41 BW_PASSWORD=$(security find-generic-password -s bw-master -w 2>/dev/null || true) 42fi 43export BW_PASSWORD 44BW_SESSION=$(bw unlock --passwordenv BW_PASSWORD --raw) 45# --passwordenv keeps the password out of ps output 46``` 47 48## Pinned MCP/tool installs 49 50```sh 51mkdir -p ~/scripts/<name>-mcp && cd ~/scripts/<name>-mcp 52npm init -y && npm install <pkg>@<exact-version> --save-exact 53# launcher: exec node "$HOME/scripts/<name>-mcp/node_modules/<pkg>/<entry>" 54``` 55 56Upgrades become explicit: bump, install, review diff, restart. 57 58## Shell hygiene 59 60- `chmod 600 ~/.zshrc ~/.zsh_history` 61- Document secrets as comments with fetch commands, never values 62- `setopt HIST_IGNORE_SPACE`; prefix sensitive commands with a space 63- A secret that ever appeared in a profile/history is burned: rotate, purge, 64 and check agent/tool session logs for copies 65 66## SSH 67 68```sh 69ssh-keygen -p -f ~/.ssh/<key> # add passphrase 70ssh-add --apple-use-keychain ~/.ssh/<key> 71# ~/.ssh/config: AddKeysToAgent yes + UseKeychain yes 72ssh-keygen -y -P "" -f ~/.ssh/<key> # MUST FAIL after 73``` 74 75## Shell script input handling 76 77- Constrain names: `[[ "$NAME" =~ ^[a-z0-9][a-z0-9-]{0,62}$ ]]` 78- Escape sed substitutions: `esc() { printf '%s' "$1" | sed 's/[&|\\]/\\&/g'; }` 79- `set -euo pipefail`; quote every expansion, especially `rm -rf "$DIR"` 80 81## API key blast radius (registrars, DNS, etc.) 82 83- Scope keys to named resources (domains), never account-wide 84- Set spend caps; disable register/transfer permission if offered 85- Record scope in the vault item notes; re-verify quarterly 86 87## Accepted residuals 88 89- stdio MCP server env is same-uid readable while running — mitigate by 90 scoping keys, not elimination 91- Agent session DBs accumulate pasted content — reference secret locations, 92 never values, in agent sessions 93`
Sign up or login to add to the discussion