diff --git a/packaging/freebsd/mother-sync-hive-keys.sh b/packaging/freebsd/mother-sync-hive-keys.sh new file mode 100755 index 0000000..a1451d4 --- /dev/null +++ b/packaging/freebsd/mother-sync-hive-keys.sh @@ -0,0 +1,87 @@ +#!/bin/sh +# mother-sync-hive-keys — rebuild the colibri user's authorized_keys from the +# `hive-pubkey-*` items that agents publish to Vaultwarden (clawdie-enable-mother). +# +# Direction: agents dial IN to this (mother) node and run `colibri-mcp`, so their +# pubkeys must be authorized here, each restricted to that one command. The file +# is REBUILT (not appended) every run, so removing an agent's vault item revokes +# its access on the next sync. +# +# Run periodically, e.g. cron (mother host): +# */5 * * * * /usr/local/bin/mother-sync-hive-keys >/dev/null 2>&1 +# +# Fail-safe: a vault/login/network failure leaves the existing authorized_keys +# untouched (the file is only replaced after a successful fetch). +# +# Tunables (env): PROVIDER_ENV, COLIBRI_HOME, COLIBRI_USER, MCP_COMMAND. + +set -eu + +PROVIDER_ENV="${PROVIDER_ENV:-/usr/local/etc/colibri/provider.env}" +COLIBRI_HOME="${COLIBRI_HOME:-/var/db/colibri}" +COLIBRI_USER="${COLIBRI_USER:-colibri}" +MCP_COMMAND="${MCP_COMMAND:-colibri-mcp}" +AUTHKEYS="${COLIBRI_HOME}/.ssh/authorized_keys" +RESTRICT="command=\"${MCP_COMMAND}\",restrict,no-pty,no-port-forwarding,no-X11-forwarding,no-agent-forwarding" + +log() { echo "mother-sync-hive-keys: $*" >&2; } + +command -v bw >/dev/null 2>&1 || { log "bw not found"; exit 4; } +command -v python3 >/dev/null 2>&1 || { log "python3 not found"; exit 4; } +[ -r "$PROVIDER_ENV" ] || { log "cannot read ${PROVIDER_ENV}"; exit 1; } + +# Load bootstrap creds (set -a so bw inherits them via the environment). +set -a +# shellcheck disable=SC1090 +. "$PROVIDER_ENV" +set +a +[ -n "${BW_CLIENTID:-}" ] && [ -n "${BW_CLIENTSECRET:-}" ] && [ -n "${BW_PASSWORD:-}" ] || + { log "provider.env lacks BW_CLIENTID/BW_CLIENTSECRET/BW_PASSWORD"; exit 1; } + +bw logout >/dev/null 2>&1 || true +bw login --apikey >/dev/null 2>&1 || true +SESS="$(bw unlock --passwordenv BW_PASSWORD --raw 2>/dev/null)" || { log "bw unlock failed"; exit 1; } +[ -n "$SESS" ] || { log "empty bw session"; exit 1; } +bw sync --session "$SESS" >/dev/null 2>&1 || true + +# Pull hive pubkeys: items named hive-pubkey-* whose notes hold an ssh key. +# Emit "type keymaterial name" (comment/extra fields dropped) for safe wrapping. +if ! KEYS="$(bw list items --search "hive-pubkey-" --session "$SESS" 2>/dev/null | python3 -c " +import sys, json +try: + data = json.load(sys.stdin) +except Exception: + data = [] +for i in data: + name = i.get('name', '') + notes = (i.get('notes') or '').strip() + if name.startswith('hive-pubkey-') and notes.startswith('ssh-'): + parts = notes.split() + if len(parts) >= 2: + print(parts[0] + ' ' + parts[1] + ' ' + name) +")"; then + log "vault fetch failed; leaving ${AUTHKEYS} untouched" + bw lock >/dev/null 2>&1 || true + exit 1 +fi +bw lock >/dev/null 2>&1 || true + +# Rebuild atomically. An empty result is legitimate (no agents → no access). +install -d -o "$COLIBRI_USER" -g "$COLIBRI_USER" -m 0700 "${COLIBRI_HOME}/.ssh" +tmp="$(mktemp "${COLIBRI_HOME}/.ssh/authorized_keys.XXXXXX")" +{ + echo "# Managed by mother-sync-hive-keys — rebuilt from Vaultwarden hive-pubkey-* items." + echo "# Manual edits are overwritten on the next sync." + printf '%s\n' "$KEYS" | while IFS= read -r line; do + [ -n "$line" ] || continue + # shellcheck disable=SC2086 + set -- $line + printf '%s %s %s %s\n' "$RESTRICT" "$1" "$2" "$3" + done +} >"$tmp" +chown "${COLIBRI_USER}:${COLIBRI_USER}" "$tmp" +chmod 0600 "$tmp" +mv "$tmp" "$AUTHKEYS" + +_count="$(printf '%s\n' "$KEYS" | grep -c . 2>/dev/null || true)" +log "rebuilt ${AUTHKEYS} with ${_count} hive key(s)"