---
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.<name>:` 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`).
