This repository has no description
_shared docs BITWARDEN-KEYCHAIN-MACOS.md
4.6 kB
Markdown
at main

Bitwarden CLI + macOS Keychain — Secret Infrastructure on macOS #

How machines in the fleet store and use Bitwarden credentials. Original setup: ole Blu (MacBook Air, ole-blu.local). Replicated on Mac Studio 2022 (mac.lan).

Architecture #

What Where Notes
Bitwarden API key (client id/secret) ~/.config/bw/env, mode 0600 Two lines, both export-ed. Non-master secret; file-safe.
Bitwarden master password macOS Keychain Service bw-master, account <user>. NEVER in a file.
Service credentials (API keys, app passwords) Bitwarden vault items Login type (type:1); fetched per-use at runtime.

Session keys (BW_SESSION) are transient — never written to disk.

Why this split #

  • The API key only identifies/authenticates the CLI to Bitwarden's API; exposure doesn't unlock the vault. It's low-sensitivity, so a 0600 file is acceptable and enables non-interactive login.
  • The master password unlocks everything. Keychain storage gives ACL-protected, prompt-on-first-access, encrypted-at-rest handling with zero file copies.
  • Scripts fetch vault secrets per-use and unset them — no export SECRET= in shell profiles (readable via ps eww, lands in every child env).

Setup on a new macOS machine #

# 1. Install CLI
brew install bitwarden-cli

# 2. Copy env file from an existing machine (secret never enters chat/session logs)
mkdir -p ~/.config/bw
scp existing-machine:.config/bw/env ~/.config/bw/env
chmod 600 ~/.config/bw/env

# 3. Log the CLI in (prints "already logged in" if done — that's fine)
source ~/.config/bw/env
bw login --apikey

# 4. Store the master password in Keychain (interactive; user types it once)
security add-generic-password -s bw-master -a "$USER" -w
#    ^ prompts: password, then confirm. Never echoed.

# 5. Verify (do not print the password itself)
security find-generic-password -s bw-master -w | wc -c   # non-zero

To get an API key if starting from scratch: https://vault.bitwarden.com → Settings → Security → Keys → View API key.

Unlocking in scripts #

Paste this block at the top of any script that needs vault access:

[ -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) || exit 1
fi
export BW_PASSWORD
bw login --apikey 2>/dev/null || true
BW_SESSION=$(bw unlock --passwordenv BW_PASSWORD --raw 2>/dev/null)

# ... do work, fetching secrets per-use (below) ...

unset BW_SESSION BW_PASSWORD   # clear when done

Fetching secrets #

Fetch the item ONCE and extract both fields (separate bw get calls can return mismatched values if the item changed between calls):

ITEM=$(bw get item "Porkbun API" --session "$BW_SESSION")
API_KEY=$(echo "$ITEM" | jq -r '.login.username')
API_SECRET=$(echo "$ITEM" | jq -r '.login.password')
# Validate — empty/null means wrong name or missing field
[ -z "$API_KEY" ] || [ "$API_KEY" = "null" ] && { echo "missing fields" >&2; exit 1; }

Password-only: bw get password "Item Name" --session "$BW_SESSION".

Item naming is a contract — scripts match exact names. Current conventions:

Bitwarden item Username field Password field
Porkbun API pk1_... API key sk1_... secret
Bluesky App Password handle app password

Pitfalls #

  • source sets shell variables, NOT environment variables. Always export BW_* vars, or bw unlock --passwordenv BW_PASSWORD silently fails (empty --raw).
  • bw login --apikey --raw returns nothing when already logged in. Use non-raw bw login --apikey and gate on bw unlock instead.
  • First Keychain read from a new process pops a macOS approval dialog ("security wants to use your confidential information") — one-time per item per session.
  • bw get matches the FIRST duplicate item name. After creating: check bw list items --search "Name" --session "$BW_SESSION" | jq 'length' → 1.
  • CLI caches vault data locally. After editing the vault elsewhere, bw sync.
  • Never paste live secret values into AI agent sessions or shell history — they persist. Reference the item name instead. Rotate + purge anything that leaked.
  • First Keychain read from a new process may pop a macOS dialog — expected.

Fleet status #

Machine Status
ole Blu (MacBook Air, ole-blu.local) Original — full setup
Mac Studio 2022 (mac.lan) Complete 2026-09-11: env via scp, login verified, Keychain item added, unlock + sync verified (664 items)