feat(mother): add mother-sync-hive-keys — rebuild authorized_keys from vault
Mother side of the vault-mediated hive key exchange (direction B — agents call mother). Pulls the hive-pubkey-* items agents publish to Vaultwarden and rebuilds the colibri user's authorized_keys, each entry restricted to the MCP command (command="colibri-mcp",restrict,no-pty,no-*-forwarding). - Rebuild, not append: deleting an agent's vault item revokes it next run. - Fail-safe: a vault/login failure leaves authorized_keys untouched. - Atomic write (mktemp + mv); colibri-owned 0600. - Tunable via PROVIDER_ENV / COLIBRI_HOME / COLIBRI_USER / MCP_COMMAND (mother = osa for now; a dedicated host is a config change). - Cron-driven (sample line in the header). Uses the bitwarden-cli-vault skill's session + authorized_keys-rebuild patterns. sh -n clean; parse/rebuild core tested (filters non-key items, strips key comments, applies the restriction wrapper). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f232bbeb69
commit
7f0782635d
1 changed files with 87 additions and 0 deletions
87
packaging/freebsd/mother-sync-hive-keys.sh
Executable file
87
packaging/freebsd/mother-sync-hive-keys.sh
Executable 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)"
|
||||
Loading…
Add table
Reference in a new issue