colibri/docs/VAULT-PROVISION-FIRST-PROOF.md
Sam & Claude b878b4bdfb
Some checks failed
CI / agent-jail-pkgs (pull_request) Has been cancelled
CI / rust (pull_request) Has been cancelled
CI / markdown (pull_request) Has been cancelled
CI / port (pull_request) Has been cancelled
docs: rewrite negative patterns as positive actionable instructions
Convert 'do not', 'cannot', 'never', 'avoid', 'don't' patterns across
AGENTS.md, README.md, and 11 docs/*.md files into positive,
actionable instructions that tell the reader what TO do.

Preserved: hard safety constraints (MUST NOT agent boundaries,
vault credential confinement intent) — these are enforceable
guardrails where the prohibition IS the instruction.
2026-06-21 13:09:19 +02:00

5.7 KiB
Raw Blame History

Vault Provision — First-Proof Runbook (osa)

Status. The spawn → vault-provision → .env chain is wired and unit-tested in code (colibri-daemon spawn hook → colibri-vault::provision), but it is not yet drivable from the CLI. Two gaps make the live proof a manual procedure today:

  • #101 — no register-tenant socket command / CLI verb; tenants are only insertable via raw SQLite.
  • #102colibri spawn-agent/spawn-local hardcode jail: None; a jailed spawn (the only kind that triggers provisioning) must be sent as raw socket JSON.

This runbook proves the chain using that interim path. When #101/#102 land, steps 34 collapse into colibri register-tenant … + colibri spawn-agent … --jail-name ….

First-proof policy (see layered-soul/docs/HIVE-ONBOARDING.md): use a scratch jail + throwaway test collection only — no real tenant data until the path hardening (#92) lands.


How the chain actually resolves (so the setup is correct)

  • The hook is provision_tenant_env(jail_name, jail_root_path). It looks up store.get_tenant(jail_name); if no tenant row matches, it no-ops.
  • It then requires tenant.jail_root_path == spawned root (trailing-slash-normalized) — a mismatch refuses provisioning.
  • It calls colibri_vault::provision(&tenant.tenant_id, jail_root_path) — so the Vaultwarden collection must be named exactly tenant_id, and tenant_id = jail name = collection name (the 1:1:1 contract).
  • On success it writes <jail_root>/.env at 0600 and flips tenant status → active.

Paths (FreeBSD daemon)

  • Socket: /var/run/colibri/colibri.sock
  • DB: /var/db/colibri/colibri.sqlite
  • Provider env (bootstrap creds): /usr/local/etc/colibri/provider.env

Prerequisites

  1. colibri-daemon running on osa.
  2. /usr/local/etc/colibri/provider.env (mode 600) has BW_SERVER plus the three bootstrap secrets BW_CLIENTID / BW_CLIENTSECRET / BW_PASSWORD (PR #69), and the daemon has them in its environment (the rc.d loads provider.env).
  3. bw CLI on the daemon's PATH.
  4. Pick a scratch tenant id, e.g. T=proof0.

Step 1 — scratch jail + bootstrap

T=proof0
sudo bastille create "$T" 15.0-RELEASE-p10 <ip>   # your standard Bastille create
sudo agent-jail-bootstrap.sh "$T"                 # runtime pkgs + colibri binaries
# jail root is /usr/local/bastille/jails/$T/root

Step 2 — test collection in Vaultwarden

In the web UI, create a Collection named exactly $T, and add one Login item:

  • Name = a harmless env var, e.g. FIRST_PROOF_KEY
  • Password field = a throwaway value (this validates the name-based contract)

The bootstrap account must have read access to that collection.

Step 3 — register the tenant (interim: manual SQLite — gap #101)

T=proof0
DB=/var/db/colibri/colibri.sqlite
sudo sqlite3 "$DB" "INSERT INTO tenants
 (tenant_id, jail_root_path, collection_id, status, created_at, updated_at)
 VALUES ('$T', '/usr/local/bastille/jails/$T/root', '$T', 'provisioned',
         datetime('now'), datetime('now'));"
  • jail_root_path must exactly match the spawned root (the hook compares them).
  • collection_id is NOT NULL UNIQUE, but the hook resolves the collection by tenant_id (name) — so set collection_id = tenant_id to satisfy the constraint (it's vestigial; see #88/#93).
  • SQLite runs in WAL, so this insert is safe while the daemon is up.

Step 4 — trigger a jailed spawn (interim: raw socket JSON — gap #102)

T=proof0
SOCK=/var/run/colibri/colibri.sock
printf '%s\n' \
 '{"cmd":"spawn-agent","provider":"local","model":"/usr/local/bin/colibri-test-agent","local_args":["--session-id","'"$T"'-proof","--step-ms","10","--hold-secs","1"],"jail":{"name":"'"$T"'","root_path":"/usr/local/bastille/jails/'"$T"'/root"}}' \
 | sudo nc -U "$SOCK"
  • provider: "local" uses the colibri-test-agent binary copied into the jail by agent-jail-bootstrap.sh, so the proof does not depend on provider API keys or a separate COLIBRI_AGENT_BINARY being present in the jail.
  • jail.namejexec into the existing jail; jail.root_path → where the hook writes .env. The provision hook fires after the local test agent spawns successfully.
  • (Equivalent: send the same JSON line with the Python raw-socket helper used by the poller.)

Step 5 — verify

T=proof0; DB=/var/db/colibri/colibri.sqlite; R=/usr/local/bastille/jails/$T/root
# daemon log shows: "provisioning tenant env from vault" then "vault provision complete"
sudo stat -f '%Sp %N' "$R/.env"                       # expect -rw------- (0600)
sudo grep -c '^FIRST_PROOF_KEY=' "$R/.env"             # expect 1 (value not printed)
sudo sqlite3 "$DB" "SELECT tenant_id,status FROM tenants WHERE tenant_id='$T';"  # expect active

Pass = .env at 0600, key present, tenant status=active.

Cleanup (scratch proof)

T=proof0; DB=/var/db/colibri/colibri.sqlite
sudo sqlite3 "$DB" "DELETE FROM tenants WHERE tenant_id='$T';"
sudo rm -f /usr/local/bastille/jails/$T/root/.env
sudo bastille destroy "$T"
# delete the test Collection + item in Vaultwarden

Security notes

  • Scratch jail + test collection only (first-proof policy) — no real tenant secrets. Bootstrap creds (BW_*) remain confined to the daemon's provider.env (0600); only the resolved .env enters the jail.

Follow-ups that retire the manual steps

  • #101 register-tenant socket command + CLI → replaces step 3.
  • #102 --jail flags on colibri spawn-agent → replaces step 4.
  • #92 path canonicalization/containment; #100 crate bw hardening (server-match, serialize) — land before promoting beyond scratch.