Merge pull request 'docs(vault): first-proof runbook for the spawn->provision chain' (#103) from docs/vault-first-proof-runbook into main
This commit is contained in:
commit
bbb1a2b075
1 changed files with 133 additions and 0 deletions
133
docs/VAULT-PROVISION-FIRST-PROOF.md
Normal file
133
docs/VAULT-PROVISION-FIRST-PROOF.md
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# 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.
|
||||
- **#102** — `colibri 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 3–4
|
||||
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
|
||||
|
||||
```sh
|
||||
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** = an env var, e.g. `DEEPSEEK_API_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)*
|
||||
|
||||
```sh
|
||||
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)*
|
||||
|
||||
```sh
|
||||
T=proof0
|
||||
SOCK=/var/run/colibri/colibri.sock
|
||||
printf '%s\n' \
|
||||
'{"cmd":"spawn-agent","provider":"deepseek","model":"deepseek-chat","jail":{"name":"'"$T"'","root_path":"/usr/local/bastille/jails/'"$T"'/root"}}' \
|
||||
| sudo nc -U "$SOCK"
|
||||
```
|
||||
|
||||
- `jail.name` → `jexec` into the existing jail; `jail.root_path` → where the hook writes
|
||||
`.env`. Use a `provider`/`model` your daemon accepts; the provision hook fires as part
|
||||
of the spawn.
|
||||
- (Equivalent: send the same JSON line with the Python raw-socket helper used by the
|
||||
poller.)
|
||||
|
||||
## Step 5 — verify
|
||||
|
||||
```sh
|
||||
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 '^DEEPSEEK_API_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)
|
||||
|
||||
```sh
|
||||
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_*`) live only in the daemon's `provider.env` (0600) and **never
|
||||
enter the jail**; the jail receives only the resolved `.env`.
|
||||
|
||||
## 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.
|
||||
Loading…
Add table
Reference in a new issue