Skip to content

WebShell

WebShell est un terminal présenté au navigateur qui atteint chaque edge enregistré via le même tunnel geminio que le reste de la plateforme utilise. Il n'y a pas de bastion SSH séparé, pas de jumpbox, pas de port entrant. L'edge continue à sortir ; le manager ouvre une classe de stream multiplexée pour l'I/O shell.

Cas d'usage :

  • L'agent suggère un fix ; vous cliquez « Open shell on edge-prod-04 » et confirmez le changement sans quitter la SPA.
  • Un vendor / contractant a besoin d'un coup d'œil ponctuel sur un host sans enrolment VPN.
  • Réponse à incident : chaque commande est enregistrée avec la ligne d'audit de la session d'origine.

Architecture

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

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

                          └─ geminio Stream (shell class)


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

Le router côté manager est dans internal/manager/biz/webshell/router.go. Il maintient un dictionnaire sessionID → Sink : les handlers WebSocket s'enregistrent à la connexion, le dispatcher tunnel-incoming route la sortie / les pushs d'exit de l'edge vers le bon navigateur.

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
}

Le handler HTTP / WebSocket vit à côté dans internal/manager/server/webshell pour que le router reste HTTP-agnostique et testable unitairement.

Les deux classes de stream

Le tunnel geminio multiplexe :

  1. Classe de contrôle — RPCs JSON (exécution de skill, signalling de plugin, sondes d'evaluator d'alerte).
  2. Classe de shell — streams d'octets bruts (un par session WebShell, un par follower tail -f, etc.).

Le découpage au niveau du tunnel importe parce que l'I/O shell est en burst et non framée ; la mélanger avec les RPCs de contrôle prive les seconds. Chaque classe a son propre budget de backpressure.

Métadonnées de session

Chaque session vivante a une 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 tique à chaque keystroke (Router.TouchInput). Un watchdog de timeout d'idle évince les sessions plus anciennes que la limite configurée sans entrée récente — défend contre la fuite « j'ai fermé l'onglet du navigateur avec une commande qui tourne ».

Enregistrement d'audit

Deux couches :

  1. Ligne d'en-tête — table webshell_sessions : qui, quand, quel edge, code de sortie, total d'octets in/out.
  2. Enregistrement de stream — l'interface Recorder côté manager prend chaque octet qui traverse le câble (les deux directions, horodaté) et l'ajoute à un fichier cast compatible asciinema sous /var/lib/ongrid/webshell-recordings/<session_id>.cast. La page admin /admin/webshell les rejoue.

L'interface Recorder est étroite exprès — la production utilise un sink fichier ; les tests injectent un fake ; les futurs backends cloud-blob drop in sans toucher au reste de la stack.

Limites de concurrence

Plafond par utilisateur : Router.CountByUser est appelé depuis le handler d'ouverture WebSocket ; les connexions au-dessus du plafond sont rejetées avec HTTP 429. Plafond par défaut 5 (configurable). Le plafond par edge défend contre un agent fou ouvrant 100 shells concurrents.

Tuer les sessions

Trois chemins tuent une session :

  1. Fermeture du navigateur — la déconnexion WebSocket propage à l'edge, qui kill -HUP le pty.
  2. Kill admin — la SPA admin appelle Killer.Kill(reason="admin terminated") sur le Sink, qui tunnel un close vers l'edge. La raison est enregistrée dans la ligne d'exit de la session.
  3. Éviction d'idle — le watchdog déclenche Kill("idle timeout") sur les sessions dont le LastInputAt a dépassé le plafond.
go
// router.go:50
type Killer interface {
    Kill(reason string)
}

Le handler côté manager installe un Killer quand il enregistre le Sink. Tout Sink qui s'opt-in devient admin-killable ; le reste n'est browser-close-killable que.

Gating de rôle

WebShell est gaté sur le rôle admin (RBAC ADR-022). Le rôle user peut chatter avec l'agent mais ne peut pas ouvrir de shells ; le rôle viewer peut lire les enregistrements de sessions passées mais ne peut pas en ouvrir de nouvelles. Le gate tourne à l'entrée du handler HTTP, avant l'upgrade WebSocket.

Voir aussi

  • Skills — le skill bash est l'équivalent one-shot de WebShell (commande unique, pas de pty). Même substrat d'audit.
  • Installation edge — démarrer l'agent edge d'un host pour que WebShell puisse l'atteindre.
  • Architecture — où se situe le tunnel geminio.