Files
wild-pc/docs/mesh.md
Paul Payne 959a896115 docs: a mesh runbook in place of the finished plan
docs/mesh.md says what runs where (castle-nats, castle-openbao, each node's
wildpc-api), how to tell it is healthy, what to do when a node goes stale, the
vault is sealed or NATS is down, and the routine tasks -- written from the live
config, not the plan. The plan is done and goes; its history is in git, and its
unfinished items are tasks in Wild Life.

AGENTS.md used the services' old names (wildpc-nats, wildpc-openbao) and
pointed at the plan for operations; it now names castle-* and points here.
2026-10-23 15:07:11 -07:00

104 lines
4.7 KiB
Markdown

# Mesh — runbook
How the nodes find each other, share config and secrets, and route to each
other's services — and what to do when that breaks. The design and its history
are in git: `git log -p -- docs/fleet-mesh-plan.md`.
## What runs where
| piece | where | what it does |
|---|---|---|
| `castle-nats` (civil) | `tls://castle-nats.civil.payne.io:4222`, token `NATS_TOKEN`; monitoring `:8222` | NATS JetStream: the mesh transport and its KV buckets |
| `castle-openbao` (civil) | `https://castle-openbao.civil.payne.io:8200`, KV-v2 mount `castle` | the secret authority behind `${secret:…}` |
| `wildpc-api` (every node) | env `WILDPC_API_NATS_ENABLED/URL/TOKEN` | the mesh client: publishes this node, watches the others |
Config and data: `/data/castle/castle-nats/config/nats-server.conf`,
`/data/castle/castle-openbao/{config/openbao.hcl,unseal.sh,data}`. Both present
the node's ACME wildcard cert (`expose.tcp.tls`), so clients verify against the
system CA; plaintext is refused.
**Nodes.** `civil` is the **authority** (`role: authority` in `wildpc.yaml`);
every other node is a follower (`primer` today). There is no consensus: when
civil is down, shared config and secrets are read-only and every node keeps
running its own deployments from local state.
**KV buckets** on NATS:
| bucket | holds | written by |
|---|---|---|
| `wildpc-registry` | each node's registry, secrets stripped | each node, for itself |
| `wildpc-presence` | a key per node, TTL 90 s, renewed on a heartbeat | each node, for itself |
| `wildpc-config` | shared fleet config | the authority only |
**Secrets.** `wildpc.yaml`'s `secrets:` block selects OpenBao (`backend: openbao`,
`mount: castle`). There is **no file fallback** in that mode — only the bootstrap
tier stays as files in `~/.wildpc/secrets/` (`OPENBAO_ROOT_TOKEN`,
`OPENBAO_UNSEAL_KEY`). civil reads with the root token; followers with a
least-privilege `wildpc-read` token, and `node_prefix` for per-node overrides.
Write with `wildpc secret set`, never by hand (see AGENTS.md §6).
## Is it healthy?
```bash
wildpc mesh status # connected? transport URL, peers
wildpc mesh nodes # each node: online | stale | offline
curl -s localhost:8222/healthz # NATS up
curl -s https://castle-openbao.civil.payne.io:8200/v1/sys/seal-status | jq .sealed
```
- **online** — the node's presence key is live.
- **stale** — no registry update from it for over 5 minutes, but not yet gone.
- **offline** — its presence key expired (90 s) or it left cleanly.
A peer's services are routed only while it is present; when it goes, its routes
drop out and the gateway answers `404 no wildpc route for this host`
immediately, rather than hanging.
## When something is wrong
**A node shows `stale` or `offline`.** On that node: is `wildpc-api` running
(`wildpc service logs wildpc-api`)? Can it reach
`castle-nats.civil.payne.io:4222` by that name (TLS checks the name, so an IP
will not do)? Does its `WILDPC_API_NATS_TOKEN` resolve to the current
`NATS_TOKEN`?
**OpenBao is sealed** (after a restart, if `unseal.sh` failed): every
`${secret:…}` read fails and `wildpc apply` refuses to converge. Unseal by hand:
```bash
curl -s -X POST https://castle-openbao.civil.payne.io:8200/v1/sys/unseal \
-d "{\"key\":\"$(cat ~/.wildpc/secrets/OPENBAO_UNSEAL_KEY)\"}" | jq .sealed
```
**NATS down.** The mesh view freezes and peers go offline; nothing else stops —
each node keeps serving. `wildpc restart castle-nats`, then
`wildpc mesh status` on each node.
**The certificate renewed and a client fails TLS.** `wildpc tls reconcile`
re-materializes the wildcard onto both services and restarts them.
## Routine tasks
**Add a node.** Install Wild PC on it; on its `wildpc-api` set
`WILDPC_API_NATS_ENABLED=true`,
`WILDPC_API_NATS_URL=tls://castle-nats.civil.payne.io:4222`,
`WILDPC_API_NATS_TOKEN=${secret:NATS_TOKEN}`; give it a `secrets:` block with a
`wildpc-read` token and `node_prefix: nodes/<host>`; leave `role` unset
(follower); `wildpc apply wildpc-api`. It appears in `wildpc mesh nodes` within a
heartbeat.
**Use a service another node provides.** `requires: - ref: <service>` in the
consumer's deployment. If no local deployment satisfies it and a present peer
does, the gateway routes `<service>.<domain>` to that peer, with a 2 s dial
timeout so a wedged peer fails fast.
**Shared config.** `wildpc mesh config list|get|set` — `set` works on the
authority only.
**Rotate the NATS token.** `wildpc secret set NATS_TOKEN`, then
`wildpc apply castle-nats` and `wildpc apply wildpc-api` on every node; peers
drop until each is re-applied.
**Cluster NATS** (not done): add a `cluster {}` block and routes to
`nats-server.conf` on each server — configuration only, by design.