--- name: client-enrollment risk_class: config_mutation inputs: [hostname, kind, role] verification: "MCP whoami(hostname) shows the entity active" docs_update_checklist: [hosts_narrative_page_if_lxc_or_vm] --- # Client enrollment Goal: bring a new host (workstation, LXC, VM) into inventory and the secrets model, with mesh membership only where it's actually needed. See [CLIENTS.md](../../../CLIENTS.md#enrollment) for the actual current flow and [operations/agent-enrollment.md](../../operations/agent-enrollment.md) for operational notes; this runbook is the risk/lifecycle framing. 1. The entity must exist in `planned`/`provisioning` state before the new host can self-enroll — add a `hosts.:` block to `seeds/inventory.yaml` and `oikos seed` to ingest it (lifecycle `planned` → `provisioning`, per [seeds/ontology.yaml](../../../seeds/ontology.yaml)). 2. Netbird join is **optional, not a required step** — only needed for hosts that must be reachable off-LAN (workstations that roam, e.g. `mac-mini`). A node reachable on the household LAN (192.168.8.0/24 — most LXCs/VMs) doesn't need it. Skip for LAN-only nodes; do it (out-of-band, console or setup key) only for hosts that need independent off-LAN reachability. 3. On the new host: run `bootstrap.sh`. This calls `POST /api/v1/clients/enroll`, which validates the entity exists and the mesh IP is in an expected subnet, then returns an age keypair and Infisical machine identity in one response — provisions `/etc/age/key.txt`, `/etc/infisical/identity`, and the context poller. 4. **Known gap, confirmed 2026-07-12: `provisioning → active` has no working path.** `EnrollClient` (`internal/httpapi/impl.go`) sets the entity's state to `provisioning`, never `active`. `bootstrap.sh` prints `POST /api/v1/clients/ws:$HNAME/activate` as the next step, but that route doesn't exist — `api/openapi.yaml` only has `/clients/enroll`, `/clients/{slug}/context`, `/clients/{slug}/secrets`. Until this is fixed (add the route, or use the generic entity PATCH to flip `state`), a freshly-enrolled client is stuck in `provisioning` — MCP `preflight` and policy's `lifecycle_overrides` for `provisioning` still apply, but nothing transitions it onward automatically. 5. Verify: MCP `whoami(hostname)` shows the entity in `active` state with its peers and health. Docs-update checklist: if the new host is an LXC/VM, add its narrative page under `containers/` or `vms/` and set `doc_page` in its inventory entry (host-level cards don't have a `doc_page` field yet — services do; narrative pages are still found via the generated `see_also` in `inventory.yaml`).