Self-Host Vaultwarden on Mac: Let AI Assistants Fetch Secrets by Name
This guide shows how to self-host Vaultwarden on a Mac using Docker and Caddy, create a read-only account for an AI coding assistant, and wrap the Bitwarden CLI in a script that unlocks, retrieves exact-match passwords by name, and immediately locks the vault, keeping secrets out of chat history and environment files.
Why Self-Host a Password Vault for AI Assistants
When an AI coding assistant needs to connect to Redis or call a third-party API, its first request is often for credentials. The easiest but insecure approach is pasting passwords into the chat or writing them into a project's .env file. Over time, secrets scatter across session logs, .env files, and any files the assistant reads with cat. Rotating a password then requires hunting down every occurrence.
Vaultwarden: A Lightweight Bitwarden-Compatible Server
Vaultwarden is a Rust implementation of the Bitwarden server API, open-source under AGPL-3.0. It is not an official Bitwarden project, but the official Bitwarden web vault, browser extensions, and the bw CLI all connect to it directly. It runs as a single container, stores data in a SQLite file, and idles at only tens of megabytes of memory. Features like organizations, collections, and member permissions are built in and used here to scope the assistant's access.
Local-Only Deployment with Docker Compose and Caddy
Two containers are orchestrated via Docker Compose in ~/vaultwarden: Vaultwarden itself (no exposed ports) and Caddy as a TLS terminator bound to 127.0.0.1:8443. This ensures the service is only reachable from the Mac itself.
services:
vaultwarden:
image: vaultwarden/server
restart: unless-stopped
environment:
DOMAIN: https://localhost:8443
SIGNUPS_ALLOWED: ${VW_SIGNUPS_ALLOWED:-false}
INVITATIONS_ALLOWED: "false"
volumes:
- ./data:/data
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "127.0.0.1:8443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./certs:/certs:roThe Caddyfile uses a self-signed certificate and proxies to Vaultwarden:
{
admin off
auto_https off
}
:443 {
tls /certs/vw.crt /certs/vw.key
reverse_proxy vaultwarden:80
}Generating and Trusting a Self-Signed Certificate on macOS
HTTPS is mandatory because browser crypto APIs used by the Bitwarden web vault only work over HTTPS. A self-signed certificate meeting macOS requirements (SAN with hostname, serverAuth EKU, validity ≤825 days) is generated with the system OpenSSL and added to the login keychain:
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -days 825 \
-subj "/CN=localhost" -keyout certs/vw.key -out certs/vw.crt \
-addext "basicConstraints=critical,CA:FALSE" \
-addext "keyUsage=critical,digitalSignature,keyEncipherment" \
-addext "extendedKeyUsage=serverAuth" \
-addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
# Trust the cert in macOS (prompts for login password)
security add-trusted-cert -r trustRoot -p ssl \
-k ~/Library/Keychains/login.keychain-db certs/vw.crtControlling Sign-Ups via Environment Variable
Vaultwarden enables registration by default. The Compose file sets SIGNUPS_ALLOWED to false via a default variable. To create accounts, temporarily override it:
VW_SIGNUPS_ALLOWED=true docker compose up -d # enable sign-up
docker compose up -d # disable after accounts are createdAfter disabling, registration attempts return Registration not allowed or user already exists.
Two Accounts: User and Read-Only Agent
Two accounts are created: one for the user, another for the AI assistant (e.g., [email protected]). The assistant's master password is a random 24-byte base64 string generated with openssl rand -base64 24 and stored in the macOS keychain:
openssl rand -base64 24 # generate random master password
security add-generic-password -a [email protected] -s vw-agent -w
# paste the password twice when promptedThe account name ( [email protected]) and service name ( vw-agent) are later used by the vw script to retrieve the password from the keychain.
Organization and Collection Permissions
Using the personal account, an organization named demo is created. Its default collection is renamed to agent. Collections act like folders with per-collection permissions. The assistant's account is invited as a "User" with access only to the agent collection and the default "View items" permission. Personal secrets (e.g., banking) stay in the user's private vault, outside the organization.
Because no email service is configured, the invitation shows "Pending confirmation." Confirming requires verifying the assistant's public-key fingerprint: run bw get fingerprint me after logging in as the assistant and match the phrase shown in the web UI.
After confirmation, the user sees all items (personal vault + organization), while the assistant sees only the two items in the agent collection. Attempts to access 个人/网银 (personal/banking) return Not found.; write attempts on demo/redis return You do not have permission to edit this item.
Wrapping the Bitwarden CLI for Exact-Match, Auto-Locking Retrieval
The official bw CLI ( npm install -g @bitwarden/cli) is used. Two problems arise when handing it directly to an assistant: bw get password searches by substring, not exact match. bw get password demo returns "Multiple items found" if multiple entries contain "demo".
After bw unlock, the session key remains valid in the environment until bw lock is called, exposing all secrets to any process that reads the variable.
A wrapper script vw solves both: it unlocks, fetches only from the designated collection, enforces exact name match via jq, and locks on exit via a trap. The master password is read from the keychain into an environment variable, never appearing in command arguments.
#!/bin/bash
# vw: fetch password from Vaultwarden by exact name. Usage: vw get <name> | vw ls
set -euo pipefail
export BITWARDENCLI_APPDATA_DIR="$HOME/.config/vw-agent" # separate from personal bw
export NODE_EXTRA_CA_CERTS="$HOME/vaultwarden/certs/vw.crt"
export BW_NOINTERACTION=true
COLLECTION_ID="<agent collection id>"
master() { security find-generic-password -a [email protected] -s vw-agent -w; }
trap 'bw lock >/dev/null 2>&1 || true' EXIT
export BW_SESSION="$(BW_PASSWORD="$(master)" bw unlock --passwordenv BW_PASSWORD --raw)"
bw sync >/dev/null
items="$(bw list items --collectionid "$COLLECTION_ID")"
case "${1:-}" in
ls) printf '%s' "$items" | jq -r '.[].name' | sort ;;
get) printf '%s' "$items" | jq -er --arg n "${2:?missing item name}" '
[.[] | select(.name == $n)]
| if length == 1 then .[0].login.password
else error("No entry or more than one named \($n)") end' ;;
*) echo "Usage: vw get <name> | vw ls" >&2; exit 1 ;;
esacThe collection ID is obtained once with bw list collections and hard-coded in the script.
Usage Examples
$ vw ls
demo/openai-key
demo/redis
$ vw get demo/redis
demo-redis-0000
$ vw get 个人/网银
jq: error (at <stdin>:0): 没有或不止一条叫 个人/网银 的条目After the command finishes, bw status shows the vault locked; the previous session key yields Vault is locked..
Performance Trade-Off
Each retrieval takes roughly 10 seconds because bw must start, unlock, and sync. This is acceptable for an assistant that fetches one or two secrets per task, but too slow for shell startup scripts.
Rules for AI Assistants
The assistant's behavior is governed by rules in its persistent instruction file ( CLAUDE.md for Claude Code, AGENTS.md for Codex):
- Passwords, tokens, API keys fetched via `vw get <project>/<service>`
- Retrieved values never written to files, command arguments, or replies
- Documentation only references entry names, not values
- Inject into environment variables for commands: `export REDISCLI_AUTH=$(vw get demo/redis)`Entry names follow a "project/service" convention (e.g., demo/redis), so the assistant knows exactly which item to request without exposing the secret in documentation.
Backup Strategy
Vaultwarden includes a backup command that exports an encrypted SQLite dump inside the container:
docker exec <vaultwarden container> /vaultwarden backup
# Backup to 'data/db_20261109_085102.sqlite3' was successfulThe data/rsa_key.pem file is also archived; losing it logs out all clients. A daily backup is saved to iCloud Drive with a two-week retention. No additional encryption is applied because item contents are already encrypted with the user's master password on the client side before reaching the server.
Limitations
This vault is only accessible on this Mac while the user is logged in and Docker is running. If the Mac is off or Docker is down, vw fails immediately. This fits a local AI assistant workflow; sharing across a team or multiple machines would require a different deployment architecture.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
