Files
sherlock/docs/gssh-integration.md
T
amacocian c9081ee2da
CI / build (push) Successful in 6s
Remove broker
2026-05-25 10:37:21 +02:00

4.1 KiB

gssh integration

How gssh-mcp will reuse the existing gssh server. Decided in Phase-0 planning; implementation in Phase 3.

Why reuse

The gssh server at gssh.alexandru.macocian.me already:

  • Authenticates inbound requests via Authentik JWTs (HttpContext.User).
  • Resolves the per-user host allow-list (SessionController.GetHostsSshConnectorService.Hosts).
  • Establishes SSH sessions internally using the host's CA-signed credentials (the operator never sees an SSH key).
  • Streams stdin/stdout as a binary WebSocket and accepts text control messages for terminal resize (SessionSocketRoute).

That's the entire job of "let an authorized human run a command on a Charlie host". Building a parallel SSH broker inside sherlock would duplicate the CA integration, the host-allow-list logic, and the audit trail — and put another piece of cert-handling code in our blast radius. We do not do that.

What gssh-mcp actually is

A thin Go HTTP+WebSocket client to the existing gssh server, wrapped in a stdio MCP. It:

  1. Reads GSSH_TOKEN from env at startup (an Authentik JWT for aud=gssh, written into the env by sherlock at agent spawn from the operator's stored TokenSet).
  2. Calls POST /api/v1/session/initialize with Authorization: Bearer <jwt> to materialise the user's session on the gssh server.
  3. Caches the list of permitted hosts from GET /api/v1/session/hosts.
  4. Exposes MCP tools:
    • ssh.list_hosts() — returns the cached host list.
    • ssh.run(host, command, timeout?) — opens a WebSocket to the session route for host, writes command + "; echo __SHERLOCK_DONE__$?\n", reads until it sees the sentinel, returns {stdout, exit_code}.
    • ssh.put_file(host, path, content) (later) — same shell channel, base64-decode + tee on the far side, sentinel as above.
  5. On 401, re-reads the keyring (calling authn.EnsureFresh, which flock-serialises against concurrent MCP refreshes) and retries once.

No SSH key handling. No cert minting. No ~/Dev/gssh shell-out.

Endpoints sherlock relies on (gssh contract)

Method Path Used for
POST /api/v1/session/initialize Materialise the user's session before opening the WebSocket.
GET /api/v1/session/hosts Per-user host allow-list.
GET /api/v1/users/me Sanity check / debug.
WS /<session-socket-route>?hostName=<host> (exact path defined in gssh's routing) Bidirectional binary stream = stdin/stdout of the SSH session. Text frames are JSON control messages (e.g. ResizeMessage).

If a gssh release ever changes one of these, gssh-mcp is the only thing in sherlock that needs to follow.

Token shape

  • Issuer: https://id.alexandru.macocian.me/application/o/gssh/
  • Audience: gssh.alexandru.macocian.me
  • Service kind in the registry: oidc-federated
  • exchange.mode = "passthrough" is safe because gssh already accepts the operator's Authentik ID token (same audience model gssh's web UI uses today). Switch to rfc8693 only if we ever introduce per-tool scope splitting (e.g. read-only vs write).

Completion sentinel — open detail for Phase 3

The gssh WebSocket is interactive (PTY-style). gssh-mcp needs a deterministic way to know a command has finished. Two options:

  1. Client-side wrap: gssh-mcp sends printf '%s\n' "<cmd>; echo __SHERLOCK_DONE__$?" | <session-shell> and parses for the marker.
  2. Server-side one-shot mode: add a small endpoint / route flag on the gssh server that runs a single command and closes the socket with the exit code in the close frame. Cleaner, but it's a gssh change.

Phase 0 decision: try (1) first because it requires no gssh change; revisit (2) if we hit edge cases (TTY echo, prompt clutter, multi-line stdout truncation).

Out of scope

  • gssh-mcp does NOT speak gssh's WebSocket protocol "directly" by hand-rolling SSH packet framing. It just speaks the documented /api/v1/session/* HTTP + WS surface.
  • gssh-mcp does NOT shell out to /mnt/seagate/Dev/gssh (which is the C# server source, not a client binary).
  • gssh-mcp does NOT enforce per-host policy locally. The gssh server already does that against JWT claims; duplicating it client-side is a footgun.