Auth
The agent’s /api/*, /ws/*, and /mcp routes are guarded by an operator gate. The gate is fail-safe: it is enabled only when a GitHub OAuth client id/secret and a master key are configured. Otherwise the agent behaves as before (open) and logs a one-time warning.
The gate is also fail-closed: when it cannot read its own config (for example the database is wedged), requests are rejected with 503 and auth config unavailable instead of falling back to the open behavior. A broken database never exposes the API.
The gate is on when all of these are true:
auth_github_client_idis set (config).STEELFRAME_GITHUB_CLIENT_SECRETis set (environment).STEELFRAME_MASTER_KEYis set (environment).auth_enabledis notfalse/0.
Access rule: the GitHub login must be in auth_allowed_users or be a member of auth_github_org (default chipcolate). The org check uses the sign-in token, which carries only read:user, so it sees public membership; list members whose org membership is private explicitly.
Managing access
Section titled “Managing access”The GUI’s Config → Users tab lists the allowed GitHub logins, adds and removes them one at a time, and edits the fallback org. It is backed by:
| Method | Path | Purpose |
|---|---|---|
GET | /api/access | Allowed users, effective fallback org, and gate state. |
PUT | /api/access | Replace allowed_users and/or github_org; omitted fields stay unchanged. |
An empty allowed_users removes the explicit list (leaving org-only access); an empty github_org restores the built-in default. Both fields are validated before anything is written, the change is recorded as access.update in the audit log, and connected clients are notified. These keys stay out of WRITABLE_CONFIG_KEYS, so PATCH /api/config cannot change who may sign in.
The remaining auth keys stay out-of-band: auth_github_client_id (OAuth app id) and auth_enabled (kill switch) are provisioned on the host, and STEELFRAME_GITHUB_CLIENT_SECRET / STEELFRAME_MASTER_KEY are environment. The first operator — and any recovery when the gate locks everyone out — is provisioned out-of-band: the TUI token works against the agent regardless of any browser session.
One origin, two doors:
- Agent origin (
infra/nginx/steelframe.conf.tpl) — serves both the agent and the console./api/,/ws/, and/mcpproxy straight to the agent port, so the agent’s own gate is the only authority (fail-closed): the console’ssf_sessioncookie for browsers, a bearer token for the TUI and automation./serves the Web GUI shell from the release channel, so the browser lands on the app, which shows its own Sign in to Steelframe GitHub gate when the session is missing. No oauth2-proxy here. - A separate GUI origin (optional,
infra/caddy/gui.caddyorinfra/nginx/gui-origin.conf.tpl) hosts the shell for hosts that want a different hostname in front of R2 — the cluster shape the dogfood host uses.
Break-glass, when an SSO gate outside the console’s own sign-in is wanted (first-operator bootstrapping, or a locked-out GUI origin): the site template carries the oauth2-proxy blocks commented at the bottom of the server, plus the auth_request/error_page lines inside location /. Uncomment them, start steelframe-oauth2, and nginx -t stays valid throughout.
Routes
Section titled “Routes”These are always mounted; login reports 404 when auth is not configured.
| Method | Path | Purpose |
|---|---|---|
GET | /api/auth/login | Redirect to GitHub OAuth |
GET | /api/auth/callback | Exchange the code, issue a session |
GET | /api/auth/logout | Clear the session cookie |
GET | /api/auth/me | Return the verified login or 401 |
Web GUI — session cookie
Section titled “Web GUI — session cookie”Sign in with GitHub. The callback sets a signed sf_session cookie:
HttpOnly; Secure; SameSite=Lax; Max-Age=14 daysThe session token is self-contained: v1.<user-b64url>.<expiry-unix>.<HMAC-SHA256>, signed with the master key. There is no server-side session store. The OAuth state carries a nonce and the redirect target in a short-lived cookie, and redirect targets are restricted to same-origin paths.
Every request is verified from the cookie (or an Authorization: Bearer). On success the middleware overwrites X-Auth-User with the verified login, so handlers and audit logs cannot see a spoofed identity.
The GUI sidebar footer loads GET /api/auth/me and shows that GitHub login (avatar from github.com/<login>.png) with a Sign out control. Desktop Sign out drops the saved bearer token and returns to the re-auth gate.
Desktop GUI — token paste
Section titled “Desktop GUI — token paste”Desktop has no cookie session to recover in-app. The re-authentication gate opens:
<agent>/api/auth/login?desktop=1&rd=/The callback renders the session token in the browser as a page for copy-paste. Paste it into the app, which sends it as Authorization: Bearer and saves it to the project registry (projects.toml).
TUI — bearer token
Section titled “TUI — bearer token”The TUI reads each project’s token from ~/.config/steelframe/projects.toml and sends it as Authorization: Bearer. It also sends X-Auth-User on every request so GitHub issue footers name the real operator.
/mcp is egress-gated by the agent’s own operator gate, exactly like /api/ — nginx only proxies it to the agent port. steelframe mcp sends the same Authorization: Bearer and X-Auth-User as the TUI. See MCP.
The agent also accepts a pre-shared automation token from STEELFRAME_TUI_TOKEN. When it matches, the request authenticates as the STEELFRAME_OPERATOR value (or automation).
Create a remote token on the host:
ssh root@HOST cat /root/steelframe-tui.token > /tmp/steelframe-tui.tokensteelframe config \ --name makolate-stack \ --url https://makolate.steelframe.chipcolate.com \ --token-file /tmp/steelframe-tui.token \ --operator your-github-loginX-Auth-User and the master key
Section titled “X-Auth-User and the master key”Resolution order for the operator identity when auth is disabled:
X-Auth-UserX-Forwarded-UserSTEELFRAME_OPERATORunknown
When auth is enabled, only the verified login survives; client-supplied identity headers are replaced.
STEELFRAME_MASTER_KEY is the HMAC key for session tokens and the source for credential encryption. Keep the same value for the life of the VM; rotating it invalidates sessions and makes existing credentials undecryptable.
Gotchas
Section titled “Gotchas”- When the gate is disabled, an unauthenticated request succeeds and the actor is
unknown. Set the identity headers or auth to get correct issue footers. 401responses carry{ "error": "unauthenticated", "login_url": "/api/auth/login" }.- Session tokens expire after 14 days; the Desktop app prompts for a fresh paste when an agent rejects the saved token.
- The agent-native OAuth App callback is
https://<HOST>/api/auth/callback— the console’s sign-in on the agent origin. A deployment that puts oauth2-proxy in front (the commented break-glass block, or a separate GUI origin) uses a second OAuth app with callbackhttps://<HOST>/oauth2/callback(seeinfra/secrets.mk.example).