Subshell Docs
Automation & MCP

System API Keys

Long-lived bearer keys for your own tooling: minting, scoping by honesty, and revoking for real.

A system API key is the credential your own tooling carries: a nightly script that opens a subshell for the repo's CI state, a home-automation hook that pings the REST API, a laptop CLI you wrote yourself. It is a long-lived bearer token; you put it in an Authorization: Bearer subshell_… header, and it authenticates forever, until you revoke it. An admin mints it under Settings → API keys (cookie session only); the mint UI, the hashing, and the revocation mechanics live on the API Keys page. This page is about what it feels like to hold one: the model, the two reads that will surprise you, and the practices the mechanism can't enforce.

What a system key is, in one honest sentence

It is a full-access machine credential with no permission map: the API has no per-key scopes to set, no read-only flag, no path allowlist. The pane's own token, by contrast, carries a scoped ceiling and dies when its subshell dies, but that credential belongs to a pane, not to you. When you mint a key you are taking the long-lived, unscoped end of that trade, because the API can't narrow it for you.

One consequence worth naming: the key can drive a terminal. It may mint a WebSocket attach token for any subshell on the instance: each mint is bound to one named pane and expires in 30 seconds, but within that bound your script can type into a live agent session on any machine the instance runs. A pane's own token can mint the same token only for itself.

What the API does enforce is the boundary that matters most: a bearer credential can never manage the instance. Your script can use Subshell; it cannot administer it. Requests to /api/users, /api/system-keys, /api/plugins, /api/network, /api/audit, the /api/admin/* group, and the subshell-sharing routes are refused (403) the moment the guard sees a bearer key instead of a browser session: no admin action, no key minting, no user changes through your script, even though the key belongs to the whole instance.

The two reads every credential gets

Everything is locked down except two deliberate, instance-wide reads that any valid credential (including a system key, including a running pane's token) can make, and you should know they exist before you assume a leaked key reads nothing:

  • The user roster. GET /api/users returns every account's email, display name, role, and disabled state. It is instance-wide by design: it is what lets the sharing picker name people, and requiring an admin session there would break that for ordinary users.
  • The address list. GET /api/settings/public carries every origin a browser may sign in from: your APP_BASE_URL, your extra TRUSTED_ORIGINS, and the addresses of any network plugin you're running: your tailnet name, your LAN name. It exists so the "Subshell for Mobile" dialog can hand your phone a working address instead of the loopback one it found you on.

The rationale is the trusted-network posture: every signed-in user is someone you admitted, and neither response carries a credential or grants access; every address it names still demands its own sign-in. The honest cost, said plainly: a leaked key now enumerates who is on your instance and where else it answers, without guessing. That's a widening, not a hole; the full accounting is in security.md §3.

Treating it like the password it is

Because there is no scope to fall back on, the practice is the security:

  • One key per purpose. Name keys so the list answers "what breaks if I disable this?" at a glance: deploy-bot, nightly-report, home-lab, not main. When one leaks you disable one thing.
  • The plaintext exists for exactly one screen. You see it once at creation and it is never retrievable; only a hash survives. If it's lost, mint a replacement; there is no reset.
  • Revoke means now. Disabling a key takes effect on its next request, not at the next restart or token expiry; deletion removes it outright. There is no grace window to worry about, which also means a fat-fingered disable is instantly visible as failures in whatever used it.
  • Rotation is mint, move, disable. No bulk export exists to help you; with hashes at rest, nobody can hand you the live keys, you or an attacker. Keep a mapping of which consumers use which key id so you can drain one when replacing it, and keep any emergency or ops key in a vault rather than in CI where every log can see it.
  • A key is not a session. It authenticates as the system service user, not as you; a subshell your script creates is owned by that account, not by you, and is not yours in the way one you launched from the browser is. That separation is useful, but it means "just use a system key for everything" quietly moves your work out of your own view.

See also

  • API Keys: the admin page: minting, disabling, deleting
  • REST API: what your key can talk to and how errors are shaped
  • Security model: the posture these keys are sized for
Edit on GitHub

Last updated on

On this page