Authentication and the remote MCP gateway¶
Off by default. A collector on a home LAN or a tailnet keeps the posture this project has always had: the network is the access control. Turn tokens on when something beyond the house can reach the collector: a corporate network, CI on another continent, or the remote MCP gateway below.
Tokens on the collector¶
On the brain's host, create the first token. The command writes a SHA-256
hash to tokens.json beside the database and prints the token once:
fleet token create ops --scope admin
fleet token create ci --scope run --expires-days 90
fleet token create mcp-gateway --scope run,delegate
fleet token list
fleet token revoke ci
Then start the collector with FLEET_AUTH=tokens, either in the process
environment or in the env object of ~/.fleet/config.json
("env": {"FLEET_AUTH": "tokens"}). The token file is re-read when it
changes, so creating and revoking need no restart.
The file is $FLEET_DATA_DIR/tokens.json (~/.fleet/data/tokens.json under
fleet up). FLEET_TOKENS_FILE moves it for the collector, and
fleet token ... --tokens-file PATH points the command at the same place.
| Scope | Allows |
|---|---|
read |
Every GET: devices, jobs, results, artifacts, the dashboard's data |
run |
Enqueue and validate jobs; cancel or retry the jobs this token started |
admin |
Everything else: other people's jobs, devices, schedules, the audit log, and the shell workload |
agent |
The runner protocol, when FLEET_AUTH_PROTOCOL=token |
delegate |
Name the person a request is for (X-Fleet-Actor). For a gateway only |
admin implies run and agent; run implies read. Nothing implies
delegate, because a token that can put a name on a job can put anyone's
name on it.
Two checks go beyond the scope, and every route makes them. A run
token may cancel or retry only a job it started; admin may act on any. And
the shell workload needs admin, however it is asked for. /api/v1, the
dashboard's POST /api/jobs/:id/cancel and /retry, and the older enqueue
routes POST /jobs and POST /api/jobs all ask the same function
(collector/src/api/policy.ts), so there is no route that skips them; until
0.9.0 the older routes skipped both. A job queued while auth was off carries
whatever name its caller claimed then, usually none, so once tokens are on it
is in practice admin's to stop. What
bounds shell besides the scope is each machine's own allowlist of script
hashes: a machine with no allowlist file never declares shell, so nothing
can be sent to it.
Runners stay open unless you close them. Phones, TVs and the browser
runner do not send tokens, so with FLEET_AUTH=tokens the runner protocol
still answers without one. FLEET_AUTH_PROTOCOL=token requires the agent
scope; set it only when every runner can send FLEET_TOKEN (runner-machine
and the host executor can).
People run fleetr login once per collector; the token is saved to
~/.fleet/credentials.json (mode 0600), keyed by URL. fleetr whoami shows
what the collector makes of it and fleetr logout forgets it; which token wins
when several are set is in AI agents and automation.
The dashboard asks for a token in a banner and exchanges it for an
HttpOnly, SameSite=Strict cookie that is honoured for reads only, so a link on
another site cannot make the browser change anything.
Every change is audited: who, through which front door (source), what,
and the result, readable at GET /api/v1/audit with admin.
The collector warns at startup about a token mode with no live tokens, or
FLEET_AUTH_PROTOCOL=token with no token holding agent.
The remote MCP gateway¶
fleet mcp (stdio) is for an agent on a machine with fleet installed.
fleet mcp --http is for everyone else: chat clients and colleagues who
reach the lab through the company's connector list. It is an OAuth 2.1
resource server speaking MCP's Streamable HTTP transport.
chat client --(OAuth access token from your IdP)--> gateway --(gateway token + X-Fleet-Actor)--> collector
- The identity provider (Entra ID, Okta...) signs people in and issues access tokens. The gateway never sees a password and never issues tokens.
- The gateway checks each token's signature against the IdP's published keys, its issuer, audience, expiry and scopes, and passes the person's name to the collector.
- The collector believes that name only because the gateway's own token
has the
delegatescope.requested_byon a job started from a chat window is the person, not the gateway.
What a remote caller cannot do, whatever their scopes: run the shell
workload, wait longer than 55 seconds in one call (a proxy would cut it), or
learn the collector's address. Calls are rate limited per person.
Run it¶
export FLEET_TOKEN=fr_... # the mcp-gateway token: run,delegate
fleet mcp --http \
--url http://collector.internal:8788 \
--host 0.0.0.0 --port 8790 \
--public-url https://fleet-mcp.example.com \
--issuer https://login.microsoftonline.com/<tenant-id>/v2.0 \
--audience <application-client-id>
Put TLS in front of it (a load balancer or reverse proxy). The gateway is stateless, so any number of replicas can sit behind one.
| Flag | Environment | Default | |
|---|---|---|---|
--url |
FLEET_RUNNER_URL, FLEET_URL |
the config | The collector, resolved the way the CLI resolves it |
--token |
FLEET_RUNNER_TOKEN, FLEET_TOKEN |
saved login | The gateway's own collector token (run,delegate) |
--host |
FLEET_MCP_HOST |
127.0.0.1 |
Address to listen on. 0.0.0.0 behind a proxy |
--port |
FLEET_MCP_PORT |
8790 | |
--issuer |
FLEET_MCP_ISSUER |
The IdP's issuer, exactly as in the token's iss |
|
--audience |
FLEET_MCP_AUDIENCE |
This resource, as in aud. Required with --issuer |
|
--jwks |
FLEET_MCP_JWKS |
discovered | Signing keys URL, if not at the issuer's OpenID configuration |
--scope-read, --scope-run |
FLEET_MCP_SCOPE_READ, _RUN |
fleet.read, fleet.run |
Which token scope or app role grants which |
--identity-claim |
FLEET_MCP_IDENTITY_CLAIM |
preferred_username,upn,email,unique_name,sub |
Which claim names the person |
--public-url |
FLEET_MCP_PUBLIC_URL |
http://host:port |
The URL clients use; the resource is <public-url>/mcp |
--allow-origin |
FLEET_MCP_ALLOW_ORIGINS |
none | Browser origins allowed to call (DNS rebinding protection) |
--calls-per-minute |
FLEET_MCP_CALLS_PER_MINUTE |
60 | Per person |
--runs-per-hour |
FLEET_MCP_RUNS_PER_HOUR |
30 | fleet_run calls per person |
--no-auth |
Development only; refused off loopback |
Endpoints: POST /mcp, GET /.well-known/oauth-protected-resource/mcp
(RFC 9728: tells a client which IdP to sign in with; also answered without the
/mcp suffix), GET /healthz. A
request without a valid token gets a 401 whose WWW-Authenticate header
points at that metadata, which is how an MCP client discovers where to log in.
At startup the gateway asks the collector what it thinks of the gateway's
token, and warns if it lacks delegate (every job would be recorded as the
gateway) or run, or if the collector runs with auth off (anyone who can
reach it directly bypasses the gateway).
Register it with the identity provider¶
Entra ID
- App registrations, New registration: "Fleet Runner MCP", single tenant.
- Expose an API: set the Application ID URI, then add two scopes,
fleet.readandfleet.run. Admin consent is the usual choice for an internal tool. - Manifest: set
"accessTokenAcceptedVersion": 2. Version 2 tokens carryiss = https://login.microsoftonline.com/<tenant-id>/v2.0andaud= the application's client id, not the URI; pass the client id to--audience. - To limit who can run jobs, use app roles instead of (or as well as)
scopes: define roles
fleet.readandfleet.run, assign groups, and the gateway reads them from therolesclaim.
Okta
- Security, API, Authorization Servers: use or add one; its issuer is
https://<org>.okta.com/oauth2/<server-id>. - Set its audience (for example
api://fleet-runner), add scopesfleet.readandfleet.run, and an access policy for the right groups. - Okta's
subis the user's login by default, which the gateway falls back to.
MCP clients find the IdP from the gateway's metadata. Many chat clients register themselves dynamically; Entra ID does not support dynamic client registration, so each client needs a pre-registered public client (redirect URI from the client's documentation) granted the two scopes. That registration, and adding the gateway to the company's approved connector list, are steps for whoever owns the tenant.
What this does not do¶
- It is not multi-tenant: one collector, one token file, one audit log.
- Rate limits are per gateway replica, in memory.
- Tokens on the collector are bearer secrets with no proof of possession. Serve the collector over TLS, or keep it on a private network, when tokens are on.