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
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`