Skip to content

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_id is set (config).
  • STEELFRAME_GITHUB_CLIENT_SECRET is set (environment).
  • STEELFRAME_MASTER_KEY is set (environment).
  • auth_enabled is not false/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.

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:

MethodPathPurpose
GET/api/accessAllowed users, effective fallback org, and gate state.
PUT/api/accessReplace 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 /mcp proxy straight to the agent port, so the agent’s own gate is the only authority (fail-closed): the console’s sf_session cookie 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.caddy or infra/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.

These are always mounted; login reports 404 when auth is not configured.

MethodPathPurpose
GET/api/auth/loginRedirect to GitHub OAuth
GET/api/auth/callbackExchange the code, issue a session
GET/api/auth/logoutClear the session cookie
GET/api/auth/meReturn the verified login or 401

Sign in with GitHub. The callback sets a signed sf_session cookie:

HttpOnly; Secure; SameSite=Lax; Max-Age=14 days

The 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 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).

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:

Terminal window
ssh root@HOST cat /root/steelframe-tui.token > /tmp/steelframe-tui.token
steelframe config \
--name makolate-stack \
--url https://makolate.steelframe.chipcolate.com \
--token-file /tmp/steelframe-tui.token \
--operator your-github-login

Resolution order for the operator identity when auth is disabled:

  1. X-Auth-User
  2. X-Forwarded-User
  3. STEELFRAME_OPERATOR
  4. unknown

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.

  • 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.
  • 401 responses 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 callback https://<HOST>/oauth2/callback (see infra/secrets.mk.example).