# Report 0017 — Rollout consolidado unibus 0.11.0 al cluster de 3 nodos (ROLLING, gate R3) - **Fecha:** 07/06/2026 - **Autor:** agente (Claude Code) - **Ámbito:** `apps/unibus` (membershipd) + `apps/unibus_admin` (panel) — cluster HA producción magnus + homer + datardos - **Estado:** done — los 3 nodos en 0.11.0, R3 pleno, panel redeployado, sin rollbacks ## Resumen Rollout rolling del binario `membershipd` 0.11.0 a los tres nodos del cluster, uno a uno con gate de reconvergencia R3 entre cada nodo, activando en el mismo ciclo de restart el monitoring NATS loopback limpio (`UNIBUS_NATS_MONITOR=1`, desacoplado del debug-log). Un solo ciclo de restart por nodo desbloqueó las tres cosas previstas: la API `/users` (panel admin, antes 404 con request firmado), las métricas NATS en `127.0.0.1:8222`, y el cierre del gap del binario viejo (report 0012). Después, re-deploy del panel admin en magnus. El cluster mantuvo quórum (2/3) en todo momento; cada nodo reconvergió a R3 (6/6 streams KV a `followers_current=2/2`) antes de tocar el siguiente. No fue necesario ningún rollback. ## Cambios | Fase | Acción | Resultado | |---|---|---| | 0 — Integrar | `git merge --no-ff quick/nats-monitor-flag` → master (commit `f31580d`), app.md → 0.11.0 | Sin conflictos; `web/` no tocado | | 0 — Verificar | `go build ./...` + tests `membership`/`client`/`embeddednats`/`cmd/membershipd` | Todo verde; push a `origin/master` | | 1 — Build | `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /tmp/membershipd_0.11.0 ./cmd/membershipd` | ELF static x86-64, sha `f6096a8e…`, `--help` OK | | 2 — Rollout | magnus → homer → datardos: backup + drop-in `UNIBUS_NATS_MONITOR=1` + binario + restart + gate R3 | 3/3 reconvergidos, sin perder quórum | | 3 — Verificar | healthz 3/3, `/users` firmado 200 3/3, 8222 3/3, sin debug, R3 6/6 a 2/2 | Todo verde | | 4 — Panel | rebuild master (Users cableado) + backup + deploy magnus + restart `unibus-admin.service` | `/api/users` 200, end-to-end vía Caddy 200 | Detalle del flag (0.11.0): función pura `natsLogOpts(debugEnv, monitorEnv) (noLog, debug, trace, monitor)` con `monitor = monitorEnv=="1" || debug` y `noLog = !debug`. Con `MONITOR=1` el endpoint de monitoring abre en `127.0.0.1:8222` (bind loopback hardcoded) dejando el log del nats-server en silencio (`NoLog` true). El acoplamiento inverso se mantiene (`DEBUG` implica `MONITOR`). Drop-in systemd aditivo: `/etc/systemd/system/membershipd-cluster.service.d/nats-monitor.conf` con `[Service]` + `Environment=UNIBUS_NATS_MONITOR=1` — no toca el unit base ni `cluster.env`. El binario nuevo es drop-in del unit existente: se verificó que acepta los 17 flags del `ExecStart` (incluido `--internal-id-file`, presente solo en el unit de datardos). No se modificó ningún unit ni `cluster.env`. ## Verificación ### Unit y baseline (confirmados, no asumidos) ``` ExecStart=/opt/unibus/membershipd … (EnvironmentFile=/opt/unibus/cluster.env) HTTP_PORT=8470 NATS_CLIENT_PORT=4250 healthz baseline (3 nodos): {"posture":{"enforce":true,"acl":true,"tls":true,"cluster":true,"store":"kv"},"status":"ok"} 8222 baseline: CERRADO en los 3 (esperado, sin drop-in) binario viejo: magnus/homer 29337645 b; datardos 29374815 b (0012, con --internal-id-file) ``` ### Hallazgo clave del gate R3 — métrica autoritativa La métrica `followers_current` de `/jsz` **solo es autoritativa consultada en el LEADER del stream**. Un nodo follower (recién reiniciado) reporta su vista desactualizada de los otros peers (`current=false`, `lag` constante) durante minutos sin que sea un problema real. La señal autoritativa por nodo es el **`/healthz` del nats-server** (`127.0.0.1:8222/healthz`): devuelve 503 si cualquier stream del que el nodo es miembro está lagging, 200 sólo cuando todos están current. Se usó ese endpoint como gate primario por nodo, y la vista del stream-leader para el conteo explícito `2/2`. ### Gate R3 por nodo (orden magnus → homer → datardos) | Nodo | Backup | membershipd healthz | nats /healthz | meta (leader/size/pending) | 8222 /varz | [DBG]/[TRC] | quórum otros 2 | |---|---|---|---|---|---|---|---| | **magnus** | `membershipd.bak.20260607-192416Z` | 200 enforce+acl+tls+cluster+kv | 200 ok | homer / 3 / 0 | 200 | 0 | homer 200, dd 200 | | **homer** | `membershipd.bak.20260607-193313Z` | 200 idem | 200 ok (t=0) | datardos / 3 / 0 | 200 | 0 | magnus 200, dd 200 | | **datardos** | `membershipd.bak.20260607-193452Z` | 200 idem | 200 ok (t=0) | homer / 3 / 0 | 200 | 0 | magnus 200, homer 200 | Re-elecciones de meta-leader observadas (esperadas, quórum 2/3 intacto): inicial `homer` → restart homer → `datardos` → restart datardos → `homer` (final). Tras completar los tres, la vista del stream-leader (magnus) confirmó los 6 buckets KV (`users/rooms/members/room_keys/rooms_by_member/nonces`) a `followers_current=2/2`: ``` streams KV a 2/2: 6/6 (stream-leader=magnus); meta-leader=homer cluster_size=3 pending=0 ``` ### FASE 3 — verificación post-rollout (3 nodos) ``` magnus homer datardos membershipd 200 200 200 posture enforce+acl+tls+cluster+store=kv (homogénea) /users firmado 200 200 200 (3 users idénticos — KV replicado R3) 8222 varz/jsz/connz 200/200/200 200/200/200 200/200/200 nats /healthz ok ok ok journal DBG/TRC 0 0 0 ``` `/users` firmado se verificó con un verificador throwaway (`client.Connect` + `ListUsers` del propio `pkg/client`, identidad operator desde `pass unibus/operator-identity` en archivo local 0600, TLS+nkey contra cada nodo por su IP pública; SAN del cert cubre las IPs). Antes del rollout, un GET `/users` firmado daba 404 (ruta ausente en el binario viejo); ahora da 200 con la allowlist: ``` operator role=admin status=active gapcheck_user role=member status=revoked (residuo E2E del report 0012) gapcheck_user2 role=member status=revoked (residuo E2E del report 0012) ``` El verificador y la copia local de la identidad se eliminaron (`cmd/usercheck` borrado del working tree; `/tmp/operator.id` con `shred`). Working tree de unibus limpio (`master...origin/master`). ### FASE 4 — deploy del panel admin ``` panel binario nuevo: /opt/unibus_admin/unibus_admin (sha 812831…), backup unibus_admin.bak.20260607-194114Z service unibus-admin.service: active; journal limpio ("users backend: control-plane", "bus TLS+nkey: ON", "cluster nodes probed: 3") GET /api/users (loopback 8480): HTTP 200 — lista real (3 users), ya no 502 GET /api/cluster: 3 nodos up:true, posture homogénea, latencias 0/10/5 ms end-to-end vía Caddy (basic auth admin): GET https://admin-…/api/users -> HTTP 200 con lista end-to-end sin auth: HTTP 401 (Caddy basic_auth protege) ``` El binario del panel se reconstruyó desde master (frontend `web/dist` ya embebido, no se tocó ningún `web/`). El Caddyfile no se modificó: el site (`reverse_proxy 127.0.0.1:8480` + `basic_auth`) quedó intacto; no hizo falta `caddy reload` (mismo puerto/config). La cadena completa navegador → Caddy(TLS+auth) → panel → bus firmado `/users` → KV R3 está probada. ## Seguridad - Secretos siempre desde `pass`/path, nunca en git ni argv: identidad operator a archivo local 0600 (borrada con `shred`); password del panel pasado a curl por stdin (`-K -`), nunca en la línea de comandos. - Monitoring vía `UNIBUS_NATS_MONITOR` (drop-in), **nunca** `UNIBUS_NATS_DEBUG`: confirmado 0 líneas `[DBG]`/`[TRC]` en journald de los 3 nodos. 8222 bind loopback hardcoded. - Backups reversibles por nodo conservados en `/opt/unibus/` (membershipd) y `/opt/unibus_admin/` (panel) — listados arriba. - Gate R3 estricto, un nodo abajo a la vez; quórum 2/3 nunca perdido. magnus (host crítico): sólo se reinició `membershipd-cluster.service`. Posture homogénea mantenida. ## Gaps / pendientes - **Verificación visual de la pestaña Users con navegador: no realizada.** Se verificó la cadena funcionalmente end-to-end (Caddy basic_auth → panel → bus → `/api/users` 200 con la lista real). No se abrió el navegador para no exponer el `admin-panel-password` en la URL (el basic_auth de Caddy obliga a meter las credenciales en la URL o en un dialog nativo no automatable de forma segura). El render de la SPA es determinista sobre `/api/users`, que responde 200. - Los usuarios `gapcheck_user`/`gapcheck_user2` (revoked) son residuos de los checks E2E del report 0012, no usuarios reales; pueden limpiarse del KV si se desea (no bloquea nada). - Binarios temporales en `/tmp` (locales y de los nodos) eliminados. Los backups `.bak.*` se conservan a propósito para reversibilidad.