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) <noreply@anthropic.com>
This commit is contained in:
@@ -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-<hash>.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.
|
||||
@@ -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-<random>.organic-machine.com {
|
||||
# basic_auth {
|
||||
# admin <HASH>
|
||||
# }
|
||||
# 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`).
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user