From 93acc059f1fa540b55cdb5729bc90d5ab45ce0dc Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 7 Jun 2026 19:44:38 +0200 Subject: [PATCH] docs: app.md + deploy artifacts (systemd unit, deploy README) - app.md: frontmatter (service, port 8480, systemd unibus-admin.service on magnus), architecture, capabilities, security, known gaps. uses_functions: sign_ed25519_go_cybersecurity. e2e_checks (build/vet/web_build/smoke_mock). - deploy/unibus-admin.service: systemd unit (Restart=always per the SIGTERM gotcha). - deploy/README.md: reproducible deploy steps (no secrets), Caddy additive-site recipe. Co-Authored-By: Claude Opus 4.8 (1M context) --- app.md | 139 ++++++++++++++++++++++++++++++++++++ deploy/README.md | 60 ++++++++++++++++ deploy/unibus-admin.service | 25 +++++++ 3 files changed, 224 insertions(+) create mode 100644 app.md create mode 100644 deploy/README.md create mode 100644 deploy/unibus-admin.service diff --git a/app.md b/app.md new file mode 100644 index 0000000..967458d --- /dev/null +++ b/app.md @@ -0,0 +1,139 @@ +--- +name: unibus_admin +lang: go +domain: infra +version: 0.1.0 +description: "Panel web de administración de unibus: un binario Go que sirve una SPA Mantine embebida y expone una REST API. Tiene la identidad ADMIN del operador, firma cada petición al plano de control del bus, y gestiona rooms, miembros, claves, usuarios y el estado del cluster." +tags: [service, messaging, admin, nats, e2e] +uses_functions: + - sign_ed25519_go_cybersecurity +uses_types: [] +framework: "" +entry_point: "main.go" +dir_path: "projects/message_bus/apps/unibus_admin" +repo_url: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/unibus_admin" +service: + port: 8480 + health_endpoint: /healthz + health_timeout_s: 3 + systemd_unit: unibus-admin.service + systemd_scope: system + restart_policy: always + runtime: systemd-system + pc_targets: + - magnus + is_local_only: false +e2e_checks: + - id: build + cmd: "CGO_ENABLED=0 go build ./..." + timeout_s: 180 + - id: vet + cmd: "CGO_ENABLED=0 go vet ./..." + timeout_s: 120 + - id: web_build + cmd: "cd web && pnpm install --frozen-lockfile && pnpm build" + timeout_s: 240 + - id: smoke_mock + cmd: "./unibus_admin --mock --port 18490 & sleep 2 && curl -fsS http://127.0.0.1:18490/healthz && curl -fsS http://127.0.0.1:18490/api/cluster >/dev/null && kill %1" + timeout_s: 30 +--- + +# unibus_admin + +Panel web de administración del bus de mensajería **unibus** (NATS + JetStream con +cifrado E2E por room). Un único binario Go que: + +1. **Sirve la SPA Mantine compilada y embebida** (`embed.FS` sobre `web/dist`), con + el mismo look índigo/oscuro que la web del cliente del bus. +2. **Expone una REST API de administración** bajo `/api`. El binario tiene la + identidad ADMIN del operador y media cada acción privilegiada contra el plano de + control de unibus, **firmando cada petición**. El navegador nunca firma, nunca + habla NATS y nunca ve una clave privada. + +## Arquitectura + +``` +navegador (SPA Mantine) + │ fetch /api/* (basic auth de Caddy + bind loopback) + ▼ +unibus_admin (gateway Go, identidad ADMIN del operador) + ├── pkg/client (unibus) → CreateRoom / Invite / Kick / ListMyRooms (firma + cripto E2E) + ├── GET firmado → /rooms/{id}/members (CanonicalRequest + SignEd25519, reusa la construcción del bus) + ├── GET /healthz (CA-pinned)→ estado + posture de los 3 nodos del cluster + └── membership.Store (opc.) → users (allowlist) cuando hay acceso directo al store + ▼ +cluster unibus (magnus + homer + datardos, enforce + ACL + TLS + KV) +``` + +El gateway reutiliza el cliente del bus (`github.com/enmanuel/unibus/pkg/client`) +para todo lo que lleva criptografía (sellar la clave de room, firmar invite/rekey), +y construye GETs firmados con la **única fuente de verdad** de la firma del bus +(`membership.CanonicalRequest` + `cs.SignEd25519`) para las lecturas que el cliente +no expone. Nunca reimplementa firma ni cripto. + +## Capacidades + +| Pestaña | Qué hace | Vía | +|---|---|---| +| **Cluster** | up/down + posture (enforce/acl/tls/cluster/store) + latencia de cada nodo | `GET /healthz` (auth-exempt) de los nodos en `--nodes`, TLS pin a la CA del bus | +| **Rooms** | listar (rooms del admin), crear (subject + E2E/persist/firmado), ver miembros, invitar, expulsar+rekey | `pkg/client` (mutaciones) + GET firmado (miembros) | +| **Users** | listar/añadir/revocar la allowlist del bus | `membership.Store` directo — sólo con `--db` (single-node) o acceso KV admin | + +## Cómo arrancar + +```bash +# Mock (iterar la SPA sin bus): +./unibus_admin --mock --port 8480 + +# Real contra un membershipd local (dev, sqlite, sin TLS): +./unibus_admin --port 8480 \ + --ctrl-url http://127.0.0.1:8470 --nats-url nats://127.0.0.1:4250 \ + --db ./local_files/unibus.db \ + --identity-pass unibus/operator-identity + +# Producción (cluster magnus, enforce + TLS + nkey): +./unibus_admin --port 8480 --bind 127.0.0.1 \ + --ctrl-url https://127.0.0.1:8470 --nats-url tls://127.0.0.1:4250 \ + --ca /opt/unibus/tls/ca.crt \ + --identity-file /opt/unibus_admin/identity.json \ + --nodes "magnus=https://127.0.0.1:8470,homer=https://141.94.69.66:8470,datardos=https://51.91.100.142:8470" +``` + +## Build + +```bash +cd web && pnpm install && pnpm build # compila la SPA a web/dist (embebida) +cd .. && CGO_ENABLED=0 go build -o unibus_admin . +``` + +## Seguridad + +- La identidad admin se carga de `pass` (`unibus/operator-identity`) o de un fichero + 0600 (`--identity-file`); nunca va hardcodeada, ni a git, ni a argv. +- El panel exige autenticación: en producción lo fronta Caddy con basic auth sobre + un subdominio ofuscado, y el gateway bindea sólo a loopback. +- Acciones destructivas (revocar user, expulsar miembro + rekey) piden confirmación + explícita en la UI. +- El operador debe estar en la allowlist del bus (rol admin) para que el gateway + pueda conectar bajo `enforce`. + +## Despliegue actual + +Desplegado en **magnus** como `unibus-admin.service` (systemd system, `Restart=always`), +puerto `8480` en loopback, fronteado por Caddy con basic auth en un subdominio +ofuscado (`admin-.organic-machine.com`). Credenciales en `pass` +(`unibus/admin-panel-password`, `unibus/admin-panel-url`). Artefactos de deploy en +`deploy/`. + +## Gaps conocidos + +- **Users en el cluster (KV)**: el plano de control no expone endpoint HTTP de users + — viven sólo en el store. Con el cluster en `--store kv`, el gateway no abre el KV + todavía, así que la pestaña Users queda degradada (estado informativo). Se habilita + con la vía de alta KV que añade la rama `quick/0011-deploy-gaps` del repo unibus, o + con `--db` en single-node. +- **meta-leader / tamaño de quórum** del cluster: `/healthz` no los expone; requieren + el endpoint de monitoreo de NATS (varz/jsz). La pestaña Cluster muestra up/posture. +- **Invite a room E2E**: requiere las claves públicas (sign_pub + kex_pub) del + invitado en hex, porque la clave de room se sella contra su X25519. La UI las pide + manualmente; no hay directorio de claves públicas todavía. diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..69ad417 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,60 @@ +# Despliegue de unibus_admin + +El panel se despliega como un service systemd en el mismo host que un nodo del +cluster unibus (hoy: **magnus**), bindeado a loopback y fronteado por Caddy con +basic auth sobre un subdominio ofuscado. No hay secretos en este directorio. + +## Requisitos en el host + +- Un nodo `membershipd` corriendo (control plane en `:8470`, NATS en `:4250`). +- La CA del bus en `/opt/unibus/tls/ca.crt` (la que firma el cert del control plane). +- La identidad ADMIN del operador como fichero JSON 0600 en + `/opt/unibus_admin/identity.json` (mismo formato que `pass unibus/operator-identity`: + `{sign_pub, sign_priv, kex_pub, kex_priv}` en base64). El operador debe estar en la + allowlist del bus con rol admin. +- Caddy activo con DNS wildcard `*.organic-machine.com` apuntando al host. + +## Pasos + +```bash +# 1. Build del binario (linux/amd64, estático) +cd web && pnpm install && pnpm build && cd .. +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o unibus_admin . + +# 2. Copiar binario + identidad al host +ssh magnus 'mkdir -p /opt/unibus_admin' +scp unibus_admin magnus:/opt/unibus_admin/unibus_admin +pass unibus/operator-identity | ssh magnus 'umask 077; cat > /opt/unibus_admin/identity.json' + +# 3. Instalar el service systemd +scp deploy/unibus-admin.service magnus:/etc/systemd/system/ +ssh magnus 'systemctl daemon-reload && systemctl enable --now unibus-admin.service' + +# 4. Verificar local en el host +ssh magnus 'curl -fsS http://127.0.0.1:8480/healthz && curl -fsS http://127.0.0.1:8480/api/cluster' + +# 5. Caddy: añadir un site ADITIVO (NO tocar bloques existentes) +PW=$(pass unibus/admin-panel-password | head -1) +HASH=$(ssh magnus "caddy hash-password --plaintext '$PW'") +ssh magnus 'cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak.$(date +%Y%m%d-%H%M%S)' +# Append el bloque (subdominio aleatorio): +# +# admin-.organic-machine.com { +# basic_auth { +# admin +# } +# reverse_proxy 127.0.0.1:8480 +# } +# +ssh magnus 'caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile && systemctl reload caddy' +``` + +## Gotchas + +- **`Restart=always`** (no `on-failure`): un `SIGTERM` limpio sale con éxito y + `on-failure` no reiniciaría — el panel quedaría muerto en silencio. +- El binario embebe la SPA (`web/dist`), así que **hay que `pnpm build` ANTES de + `go build`**; si no, embebe el placeholder. +- La identidad del operador es un secreto: 0600, nunca a git ni a argv. +- El subdominio y la password viven en `pass` (`unibus/admin-panel-url`, + `unibus/admin-panel-password`). diff --git a/deploy/unibus-admin.service b/deploy/unibus-admin.service new file mode 100644 index 0000000..c2f4642 --- /dev/null +++ b/deploy/unibus-admin.service @@ -0,0 +1,25 @@ +[Unit] +Description=unibus admin panel (web administration gateway) +Documentation=https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/unibus_admin +After=network-online.target membershipd-cluster.service +Wants=network-online.target + +[Service] +Type=simple +User=root +WorkingDirectory=/opt/unibus_admin +ExecStart=/opt/unibus_admin/unibus_admin \ + --port 8480 --bind 127.0.0.1 \ + --ctrl-url https://127.0.0.1:8470 \ + --nats-url tls://127.0.0.1:4250 \ + --ca /opt/unibus/tls/ca.crt \ + --identity-file /opt/unibus_admin/identity.json \ + --nodes "magnus=https://127.0.0.1:8470,homer=https://141.94.69.66:8470,datardos=https://51.91.100.142:8470" +# Restart=always (NO on-failure): un SIGTERM limpio sale con exit success y +# on-failure NO reiniciaria, dejando el panel muerto en silencio. +Restart=always +RestartSec=2 +LimitNOFILE=65536 + +[Install] +WantedBy=multi-user.target