Skip to content

WebShell

WebShell es una terminal cara al navegador que alcanza cada edge registrado a través del mismo túnel geminio que usa el resto de la plataforma. No hay bastión SSH separado, no hay jumpbox, no hay puerto entrante. El edge sigue marcando hacia afuera; el manager abre una clase de stream multiplexado para I/O de shell.

Casos de uso:

  • El agente sugiere un fix; haces clic en "Abrir shell en edge-prod-04" y confirmas el cambio sin salir de la SPA.
  • Un vendor / contratista necesita una mirada one-off a un host sin enrolarse en la VPN.
  • Respuesta a incidentes: cada comando queda grabado con la fila de audit de la sesión originante.

Arquitectura

text
browser ──WebSocket──> manager:/v1/webshell/ws

                          ├─ Router.Register(sessionID, sink, ActiveSession)

                          └─ geminio Stream (shell class)


                              edge agent
                                  └─ pty.Start("/bin/bash")

El router del lado del manager está en internal/manager/biz/webshell/router.go. Mantiene un directorio sessionID → Sink: los handlers de WebSocket se registran al conectar, el dispatcher tunnel-incoming rutea las pushes de salida / exit del edge al navegador correcto.

go
// internal/manager/biz/webshell/router.go:57
type Router struct {
    mu          sync.RWMutex
    sinks       map[string]Sink
    meta        map[string]*ActiveSession // sessionID → metadata
    stdoutBytes sync.Map                  // sessionID → *uint64
}

El handler HTTP / WebSocket vive al lado en internal/manager/server/webshell para que el router se mantenga agnóstico de HTTP y unit-testable.

Las dos clases de stream

El túnel geminio multiplexa:

  1. Clase de control — JSON RPCs (ejecución de skill, señalización de plugin, probes del evaluator de alertas).
  2. Clase de shell — streams de bytes crudos (uno por sesión WebShell, uno por follower de tail -f, etc.).

Dividir a nivel de túnel importa porque el I/O de shell es bursty y sin framing; mezclarlo con los RPCs de control mata a estos últimos. Cada clase tiene su propio presupuesto de backpressure.

Metadata de sesión

Cada sesión viva tiene un ActiveSession:

go
// router.go:37
type ActiveSession struct {
    SessionID    string
    OngridUserID uint64
    SSHUser      string
    DeviceID     uint64
    EdgeID       uint64
    StartedAt    time.Time
    LastInputAt  time.Time // updated on every browser → edge frame
}

LastInputAt tickea en cada keystroke (Router.TouchInput). Un watchdog de idle-timeout desaloja sesiones más viejas que el límite configurado sin input reciente — defiende contra el leak "cerré la pestaña del browser con un comando corriendo".

Grabación de auditoría

Dos capas:

  1. Fila de header — tabla webshell_sessions: quién, cuándo, qué edge, exit code, total de bytes in/out.
  2. Grabación de stream — la interfaz Recorder del lado del manager toma cada byte que cruza el wire (ambas direcciones, timestamped) y anexa a un archivo cast compatible con asciinema bajo /var/lib/ongrid/webshell-recordings/<session_id>.cast. La página admin /admin/webshell los reproduce.

La interfaz Recorder es estrecha a propósito — producción usa un sink de archivo; los tests inyectan un fake; futuros backends de cloud-blob caen sin tocar el resto del stack.

Límites de concurrencia

Tope por-usuario: Router.CountByUser se llama desde el handler de WebSocket open; las conexiones por encima del tope son rechazadas con HTTP 429. El tope default es 5 (configurable). El tope por-edge defiende contra un agente runaway abriendo 100 shells concurrentes.

Matando sesiones

Tres rutas matan una sesión:

  1. Cierre del browser — la desconexión del WebSocket se propaga al edge, que hace kill -HUP al pty.
  2. Kill por admin — la SPA admin llama Killer.Kill(reason="admin terminated") en el Sink, que tunelea un close hasta el edge. La razón queda grabada en la fila de exit de la sesión.
  3. Eviction por idle — el watchdog dispara Kill("idle timeout") en sesiones cuyo LastInputAt excedió el tope.
go
// router.go:50
type Killer interface {
    Kill(reason string)
}

El handler del lado del manager instala un Killer cuando registra el Sink. Cualquier Sink que opte queda admin-killable; el resto solo es browser-close-killable.

Gating por rol

WebShell está gated en el rol admin (RBAC de ADR-022). El rol user puede chatear con el agente pero no puede abrir shells; viewer puede leer grabaciones de sesiones pasadas pero no puede abrir nuevas. El gate corre en la entrada del handler HTTP, antes del upgrade a WebSocket.

Ver también

  • Skills — el skill bash es el equivalente one-shot de WebShell (un comando, sin pty). Mismo sustrato de audit.
  • Instalación de edge — levantar el agente edge de un host para que WebShell pueda alcanzarlo.
  • Arquitectura — dónde se sienta el túnel geminio.