Address the 5 review concerns on the secrets-out-of-the-box feature: 1. Seed↔fetch path alignment: _seed_split_env routes BW_* creds out of .env into ~/.config/vault-bootstrap.env (SEED_VAULT_BOOTSTRAP_REL), the path clawdie-vault-fetch actually reads — so 'seed bootstrap → fetch out of the box' now lines up without an explicit --bootstrap arg. 2. Drop unused COLLECTION_ID from clawdie-vault-fetch. Items are fetched by name via 'bw get password', which is fail-closed on ambiguity; document that item names must be unique in the visible vault. 3. Agent dir validation: _seed_agent_name_ok rejects leading-dot dirs (.Spotlight-V100, .fseventsd) and traversal; _seed_agent_has_payload requires a recognized payload so an empty/stray dir can't become active. 4. No phantom homes: extra agent dirs stage under /var/db/clawdie/seed/<agent> only — _seed_stage_agent never writes a home or SSH keys. 5. Bootstrap file mode enforcement: clawdie-vault-fetch now stat-checks the bootstrap file and refuses group/world-readable unless VAULT_ALLOW_INSECURE_BOOTSTRAP is set. Also renames _seed_import_env → _seed_merge_env + _seed_split_env and adds _seed_key_ok to guard env var names. Checks: sh -n on vault-fetch/live-seed/build.sh; git diff --check; ./scripts/check-format.sh (prettier clean); 5 concerns verified present. Co-Authored-By: Hermes & Sam <hello@clawdie.si>
170 lines
6.5 KiB
Bash
170 lines
6.5 KiB
Bash
#!/bin/sh
|
|
# clawdie-vault-fetch — pull agent secrets from Vaultwarden into the host.
|
|
#
|
|
# Language-neutral bridge between the Clawdie Vaultwarden instance and a host's
|
|
# secret store. The TS post-install setup flow shells out to this; the live USB
|
|
# and firstboot can call it directly. It depends only on the `bw` (Bitwarden)
|
|
# CLI — no node module, no jq — so the same helper serves the deployed disk and
|
|
# the live operator image (see docs in clawdie-ai/docs/VAULTWARDEN-SETUP.md).
|
|
#
|
|
# Contract: each secret is one login item in the `agent-secrets` collection
|
|
# whose ITEM NAME is exactly the env var name (e.g. ANTHROPIC_API_KEY) with the
|
|
# value in the password field. `bw get password <NAME>` then returns it raw.
|
|
#
|
|
# Bootstrap (the one secret that can't live in the vault) is read from a 0600
|
|
# env file holding BW_CLIENTID / BW_CLIENTSECRET / BW_PASSWORD. Absent file =>
|
|
# nothing to do (exit 3), so the manual setup wizard remains the floor.
|
|
#
|
|
# Usage:
|
|
# clawdie-vault-fetch # print KEY=VALUE lines to stdout
|
|
# clawdie-vault-fetch --write-env FILE # upsert results into FILE (0600)
|
|
# clawdie-vault-fetch --bootstrap FILE # explicit bootstrap env file
|
|
# clawdie-vault-fetch --keys "A B C" # override the key name list
|
|
#
|
|
# Exit codes (so callers can distinguish "skip" from "broken"):
|
|
# 0 ran cleanly (zero or more keys printed/written)
|
|
# 1 vault was configured but login/unlock/fetch failed
|
|
# 3 no bootstrap config found — caller should fall back to manual entry
|
|
# 4 `bw` CLI not installed
|
|
set -eu
|
|
|
|
SERVER="${VAULT_SERVER:-https://vault.smilepowered.org}"
|
|
# NOTE: items are fetched by name with `bw get password`, which is fail-closed
|
|
# on ambiguity (multiple matches error out). Item names must therefore be unique
|
|
# in the agent account's visible vault — see docs/VAULTWARDEN-SETUP.md. We do not
|
|
# scope by collection here to avoid a JSON-parse (jq) dependency.
|
|
BOOTSTRAP_FILE="${VAULT_BOOTSTRAP_FILE:-${HOME}/.config/vault-bootstrap.env}"
|
|
WRITE_ENV=""
|
|
|
|
# Provider key names mirror clawdie-ai's PROVIDER_KEY_BY_PROVIDER (the non-null
|
|
# ones). Extend via --keys or VAULT_FETCH_KEYS without editing this file.
|
|
KEYS="${VAULT_FETCH_KEYS:-ANTHROPIC_API_KEY OPENAI_API_KEY OPENROUTER_API_KEY ZAI_API_KEY DEEPSEEK_API_KEY GEMINI_API_KEY GROQ_API_KEY}"
|
|
|
|
usage() {
|
|
echo "usage: clawdie-vault-fetch [--write-env FILE] [--bootstrap FILE] [--keys \"K1 K2\"]"
|
|
exit "${1:-0}"
|
|
}
|
|
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--write-env) WRITE_ENV="${2:?--write-env needs a value}"; shift 2 ;;
|
|
--bootstrap) BOOTSTRAP_FILE="${2:?--bootstrap needs a value}"; shift 2 ;;
|
|
--keys) KEYS="${2:?--keys needs a value}"; shift 2 ;;
|
|
-h|--help) usage 0 ;;
|
|
*) echo "clawdie-vault-fetch: unknown argument: $1" >&2; usage 1 ;;
|
|
esac
|
|
done
|
|
|
|
log() { echo "clawdie-vault-fetch: $*" >&2; }
|
|
|
|
# No vault intent on this host => skip quietly so the manual path is unaffected.
|
|
# Checked before the bw probe: a host with no bootstrap drop shouldn't care
|
|
# whether the CLI is installed.
|
|
if [ ! -f "$BOOTSTRAP_FILE" ]; then
|
|
log "no bootstrap file at $BOOTSTRAP_FILE — skipping vault fetch"
|
|
exit 3
|
|
fi
|
|
|
|
# The bootstrap file holds the vault master password. Refuse to read it if it is
|
|
# group/world-readable, unless explicitly overridden. stat differs across BSD
|
|
# (-f '%Lp') and GNU (-c '%a'); a non-octal/unknown result skips the check.
|
|
_mode="$(stat -f '%Lp' "$BOOTSTRAP_FILE" 2>/dev/null || stat -c '%a' "$BOOTSTRAP_FILE" 2>/dev/null || echo '')"
|
|
case "$_mode" in
|
|
''|*[!0-7]*) _mode='' ;;
|
|
esac
|
|
if [ -n "$_mode" ] && [ "$(( 0$_mode & 077 ))" -ne 0 ]; then
|
|
if [ -n "${VAULT_ALLOW_INSECURE_BOOTSTRAP:-}" ]; then
|
|
log "WARNING: $BOOTSTRAP_FILE is mode $_mode (group/world-readable) — proceeding (override set)"
|
|
else
|
|
log "refusing: $BOOTSTRAP_FILE is mode $_mode (group/world-readable). chmod 600 it, or set VAULT_ALLOW_INSECURE_BOOTSTRAP=1"
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
if ! command -v bw >/dev/null 2>&1; then
|
|
log "bw (Bitwarden CLI) not found — install @bitwarden/cli first"
|
|
exit 4
|
|
fi
|
|
|
|
# Load bootstrap creds without echoing them. set -a so they reach bw via env.
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
. "$BOOTSTRAP_FILE"
|
|
set +a
|
|
|
|
if [ -z "${BW_CLIENTID:-}" ] || [ -z "${BW_CLIENTSECRET:-}" ] || [ -z "${BW_PASSWORD:-}" ]; then
|
|
log "bootstrap file is missing BW_CLIENTID / BW_CLIENTSECRET / BW_PASSWORD"
|
|
exit 1
|
|
fi
|
|
|
|
WORK="$(mktemp -d "${TMPDIR:-/tmp}/clawdie-vault.XXXXXX")"
|
|
# Lock the vault on any exit; never leave an unlocked session behind.
|
|
cleanup() {
|
|
bw lock >/dev/null 2>&1 || true
|
|
rm -rf "$WORK"
|
|
}
|
|
trap cleanup EXIT INT TERM
|
|
|
|
bw config server "$SERVER" >/dev/null 2>&1 || {
|
|
log "could not set bw server to $SERVER"
|
|
exit 1
|
|
}
|
|
|
|
# login --apikey reads BW_CLIENTID/BW_CLIENTSECRET from env. Tolerate the
|
|
# "already logged in" case so repeat runs don't fail.
|
|
if ! bw login --apikey >/dev/null 2>"$WORK/login.err"; then
|
|
if ! grep -qi 'already logged in' "$WORK/login.err"; then
|
|
log "bw login failed:"
|
|
sed 's/^/ /' "$WORK/login.err" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
SESSION="$(bw unlock --raw --passwordenv BW_PASSWORD 2>"$WORK/unlock.err")" || {
|
|
log "bw unlock failed:"
|
|
sed 's/^/ /' "$WORK/unlock.err" >&2
|
|
exit 1
|
|
}
|
|
if [ -z "$SESSION" ]; then
|
|
log "bw unlock returned an empty session"
|
|
exit 1
|
|
fi
|
|
|
|
# Make sure local cache reflects the server (web-UI edits don't sync otherwise).
|
|
bw sync --session "$SESSION" >/dev/null 2>&1 || true
|
|
|
|
# Collect KEY=VALUE pairs for every key name that resolves to a value. A missing
|
|
# item is not an error — the operator may only have stored a subset.
|
|
found=0
|
|
out="$WORK/out.env"
|
|
: >"$out"
|
|
for key in $KEYS; do
|
|
val="$(bw get password "$key" --session "$SESSION" 2>/dev/null)" || continue
|
|
[ -n "$val" ] || continue
|
|
printf '%s=%s\n' "$key" "$val" >>"$out"
|
|
found=$((found + 1))
|
|
done
|
|
|
|
log "resolved $found of $(echo "$KEYS" | wc -w | tr -d ' ') key(s) from agent-secrets"
|
|
|
|
if [ -n "$WRITE_ENV" ]; then
|
|
# Upsert into the target env file at 0600 without disturbing other keys.
|
|
touch "$WRITE_ENV"
|
|
chmod 0600 "$WRITE_ENV"
|
|
merged="$WORK/merged.env"
|
|
cp "$WRITE_ENV" "$merged" 2>/dev/null || : >"$merged"
|
|
while IFS= read -r line; do
|
|
k="${line%%=*}"
|
|
# Drop any existing definition of this key, then append the new one.
|
|
grep -v "^${k}=" "$merged" >"$merged.tmp" 2>/dev/null || : >"$merged.tmp"
|
|
mv "$merged.tmp" "$merged"
|
|
printf '%s\n' "$line" >>"$merged"
|
|
done <"$out"
|
|
cp "$merged" "$WRITE_ENV"
|
|
chmod 0600 "$WRITE_ENV"
|
|
log "wrote $found key(s) into $WRITE_ENV"
|
|
else
|
|
cat "$out"
|
|
fi
|
|
|
|
exit 0
|