feat(mother): add mother-sync-hive-keys — rebuild authorized_keys from vault #140

Merged
clawdie merged 1 commit from mother-sync-hive-keys into main 2026-06-21 20:31:17 +02:00

View file

@ -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)"