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:
gssh(Confidential) — the long-standing provider that powers Gssh's browser PTY login via server-side OIDC code flow.sherlock-cli(Public, PKCE) — a separate provider sherlock authenticates against from the CLI. No client secret, redirect URI pinned tohttp://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:
- 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'sOIDC__AllowedRolescheck reads it).
- 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.