Files
sherlock/docs/auth-model.md
T
amacocian 24f77e7b74 Phase 1: login broker + agent-agnostic wrapper
Lock and implement Phase 1 decisions:

- #8 token storage: OS keyring (zalando/go-keyring) with strict probe at
  startup of both sherlock and sherlock-broker; fail fast with exit 3
  and a per-OS hint if Secret Service / Keychain / Credential Manager
  is missing.
- #9 RPC framing: JSON-over-newline on the UDS at
  $XDG_RUNTIME_DIR/sherlock.sock, debuggable with socat.
- #10 broker lifecycle: forked child process (setsid-detached), per-user
  PID-file flock prevents double-start, auto-exit after
  SHERLOCK_BROKER_IDLE (default 1h). No systemd.
- #11 loopback port: 127.0.0.1:6990 for the Authentik PKCE callback.
  Actual Authentik provider creation deferred; login_start returns a
  clean 'not_configured' error mentioning the env vars to set, and the
  full OIDC path is exercised by an integration test against a stub
  Authentik (httptest + go-jose ES256 signer).

New packages (all green under `go test -race`):
- internal/xdg, internal/rpc, internal/socket — primitives
- internal/keyring (+ fake/) — Probe, Store, TokenSet
- internal/authn — discovery, PKCE, loopback flow, single-flight refresh
- internal/broker — lifecycle, server, spawn, RPC methods
- internal/agent — TOML profile loader (embedded + user overlay),
  MCP-config renderer, argv/env builder, syscall.Exec wrapper

CLI:
- cmd/sherlock: login / logout [--shutdown] / status / run <agent> /
  <agent-name> alias dispatch
- cmd/sherlock-broker: daemon subcommand wiring all of the above

Deps: zalando/go-keyring, BurntSushi/toml, coreos/go-oidc/v3,
golang.org/x/oauth2, golang.org/x/sync. go directive bumped to 1.25;
CI Go version bumped to 1.26.3 to match.

Docs: new docs/storage.md and docs/rpc.md; auth-model, conventions,
README, plan.md all updated to reflect the locked decisions.

End-to-end verified locally: auto-spawn broker, status, login refused
with not_configured, agent alias execs through with the rendered MCP
config path, logout --shutdown brings the socket down.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-24 22:39:34 +02:00

62 lines
3.0 KiB
Markdown

# Auth model
How sherlock obtains and distributes credentials.
## The two layers
1. **Operator → Authentik.** A single OAuth 2.1 authorization-code + PKCE flow against Authentik (`id.alexandru.macocian.me`). The broker owns the resulting ID/access/refresh tokens. This is the only browser flow per workstation per session.
2. **Broker → downstream service.** For each per-service request from an MCP, the broker mints a service-scoped token using one of the three grant kinds below.
## Grant kinds
Set per service in `~/.config/sherlock/services.d/<name>.toml` (see [service-registry.md](service-registry.md)).
### `oidc-federated`
The downstream service already federates against Authentik (Gitea, Grafana, Caddy-protected apps). The broker either:
- Hands the existing Authentik-issued ID token (when the service's expected audience matches), or
- Performs **RFC 8693 token exchange** against Authentik's token endpoint, swapping the operator's ID token for a service-scoped access token.
The MCP receives the token in an env var (e.g. `GITEA_TOKEN`).
### `own-oauth`
The service runs its own OAuth/OIDC stack, not federated through Authentik (think GitHub.com, a SaaS API, …). The broker:
- Holds a per-(operator, service) OAuth client registration.
- Runs the auth-code + PKCE flow in a browser the first time, with the callback received on the broker's loopback listener.
- Caches the refresh token alongside the Authentik creds.
- Refreshes silently after that.
Triggered explicitly via `oauth.consent(service)` from `sherlock-mcp` (Phase 4) or via `sherlock login --service <name>`.
### `static-pat`
For services that have no OAuth at all, or where the only sane option is a long-lived PAT. The broker reads the PAT from an `age`-encrypted blob and injects it. Avoid where possible; document why every time it's used.
## Token storage
See [storage.md](storage.md) for the full decision (OS keyring via
`zalando/go-keyring`, pre-flight at startup, exit code 3 on failure).
- Authentik creds persist in the OS keyring (Decision #8); per-service
tokens follow the same pattern in Phase 2+.
- In-memory cache mirrors the keyring; writes happen on rotation.
- Refresh is single-flight (`golang.org/x/sync/singleflight`): a 401
storm across N MCPs collapses to one refresh.
## Audience binding
Per the MCP 2025-06-18 spec, downstream tokens MUST be audience-bound. The broker never hands an MCP a token whose `aud` claim doesn't name that service. This is the "confused deputy" mitigation the spec calls out.
## Scope minimisation
Default service registrations request read-only scopes. Write scopes require either:
- The operator passing `--write` to `sherlock run` (Phase 5), or
- An explicit `scopes` override in the service's TOML.
## Out of scope for sherlock
- Issuing SSH certs (gssh handles this — [gssh-integration.md](gssh-integration.md)).
- Multi-user / shared tenancy. Sherlock is per-operator, full stop.
- Acting as a Resource Server itself (Phase 4's `sherlock-mcp` is a stdio MCP, not HTTP — the agent CLI talks to it over a pipe, no inbound auth).