Files
sherlock/docs/gssh-mcp.md
T
amacocian 22ca6b3dc7
CI / build (push) Failing after 0s
Setup gssh-mcp
2026-05-30 14:11:01 +02:00

5.9 KiB

gssh-mcp

sherlock's MCP server for Gssh, the homelab SSH gateway. Lets an agent run a single shell command on an allow-listed host without standing up a PTY, using the operator's own JWT-authenticated session.

How auth works

Two Authentik OIDC providers front the same Gssh deployment:

  1. gssh (Confidential) — the long-standing provider that powers Gssh's browser PTY login via server-side OIDC code flow.
  2. sherlock-cli (Public, PKCE) — a separate provider sherlock authenticates against from the CLI. No client secret, redirect URI pinned to http://127.0.0.1:6990/callback.

Gssh's JwtBearer scheme accepts JWTs from either provider via comma-separated OIDC__ApiAudience + OIDC__BearerIssuers. JWKS verification is anchored on a single discovery URL — all providers in an Authentik tenant share the signing key, so one fetch validates both.

The sherlock-cli provider can be reused by every future Authentik-fronted MCP: each backing service just adds the provider's audience to its own ValidAudiences and the provider's issuer to its own ValidIssuers. One wallet entry, one OAuth browser pop, N services.

One-time setup

Authentik

Create a second OIDC provider:

  1. Applications → Providers → Create → OAuth2/OpenID Provider:
    • Client type: Public
    • Redirect URI (Strict): http://127.0.0.1:6990/callback
    • Subject mode: Based on the User's username (the default "hashed user ID" mode produces a sub Gssh can't SSH as)
    • Scope mappings: include the standard openid/profile/email and a mapping that emits groups (Gssh's OIDC__AllowedRoles check reads it).
  2. Applications → Applications → Create an app that points at the new provider. Slug = sherlock-cli (the slug is the trailing path segment in the issuer URL).

Gssh

Add the new audience and issuer to Gssh's env (alongside the existing gssh-provider values):

OIDC__ApiAudience=<gssh-client-id>,<sherlock-cli-client-id>
OIDC__BearerIssuers=https://id.example/application/o/gssh/,https://id.example/application/o/sherlock-cli/

Build & install

For Charlie (default — Authentik client ID embedded in the binary):

cd ~/Dev/charlie/sherlock
go install ./cmd/gssh-mcp

That's it. The Charlie Authentik sherlock-cli client ID lives in cmd/gssh-mcp/gssh_clientid_charlie.go and is baked in unless you build with -tags noembed.

For other deployments:

go build -tags noembed \
  -ldflags "-X main.gsshIssuer=https://id.example/application/o/sherlock-cli/ \
            -X main.gsshClientID=<CLIENT_ID>                                    \
            -X main.gsshBaseURL=https://gssh.example"                           \
  ./cmd/gssh-mcp

Knobs

Variable / -X main.… Charlie default
gsshIssuer https://id.alexandru.macocian.me/application/o/sherlock-cli/
gsshBaseURL https://terminal.alexandru.macocian.me
gsshClientID embedded; see gssh_clientid_charlie.go
gsshClientSecret "" (provider is Public, PKCE-only — no secret involved)
Version 0.0.0-dev

Verify (without an agent)

gssh-mcp --probe

First run: opens a browser, you click through Authentik's "Authorize Sherlock-Cli" page, browser shows "Logged in. You may close this tab.", terminal prints:

OK: logged in to https://terminal.alexandru.macocian.me as <name> (account=<username>, roles=[...])

Subsequent runs are silent until the refresh window opens.

To force a re-login: sherlock logout gssh.

Use with an agent

sherlock copilot (or sherlock claude) automatically renders an MCP config that lists gssh-mcp. Copilot spawns it as a stdio subprocess; the first call into a tool triggers the OAuth flow above.

Tools

Tool Description
list_hosts SSH hosts this operator is allowed to connect to.
initialize_session Force ephemeral 5-min SSH cert creation (idempotent).
run_command Run a single shell command on a host; returns stdout / stderr / exit code (or timed_out after the server's 60-second cap).

run_command opens a fresh WebSocket per call to /api/v1/exec/{host}, sends a single Start datagram carrying the UTF-8 command, then demuxes streamed Stdout / Stderr datagrams into capped buffers until an Exit (carries ASCII exit code) or Timeout datagram arrives. Wire protocol lives in internal/wsdatagram/ and mirrors Gssh's Models/ShellDatagram.cs exactly (fixed 2048-byte frame, 1-byte op + 2-byte length + 2045-byte payload).

Buffer caps

Stream Cap When exceeded
stdout 256 KiB trailing bytes dropped, stdout_truncated=true
stderr 64 KiB trailing bytes dropped, stderr_truncated=true

A timed_out=true result means the remote command exceeded the server-side 60-second hard cap. The client tool itself has a 90 s deadline so the agent isn't left hanging if the connection or the server stall mid-stream.

Debugging

SHERLOCK_DEBUG_LOG=/tmp/gssh-mcp.log gssh-mcp --probe
tail -f /tmp/gssh-mcp.log

Every HTTP request, WS dial, Start datagram, and terminal datagram gets one line. Empty SHERLOCK_DEBUG_LOG = no logging, no file touched.

When debugging Gssh-side rejections, look at Gssh's container logs — 401s carry the JwtBearer failure reason (audience mismatch, issuer mismatch, expired, …) inline.