Saphan StudioDocs
The console

What the console does not protect against

The limits, stated as limits — including the two things the terminal does not promise and the one named cookie exception.

The rule, in full:

ConnectionSecureWhy
HTTPS (or behind a proxy sending X-Forwarded-Proto: https)onThe normal case.
Plain HTTP from a loopback address (127.0.0.1, ::1)offThe fleet serves plain HTTP on localhost by standing owner decision. A Secure cookie is discarded by every browser on such a connection, so login could not work at all — and it would fail with nothing to see.
Plain HTTP from anything elseonThe cookie will be discarded and login will fail. That is the correct failure. A console reachable in plaintext from off-host is the thing that must not quietly work.

"Loopback" means the address the server observed on the connection — the peer address of the socket. It is deliberately not the Host: header, and not the name you typed in the browser. The header is set by the client; a remote client that sends Host: localhost gets row three, a Secure cookie, and a page that says so.

HttpOnly and SameSite=Lax are unconditional. Path=/console keeps the session cookie away from /dashboard, /mcp and every other surface.

The login page always states which of the three rows it is in. You never have to guess, and a silently insecure cookie is never what you got.

What this does not protect against

Written here rather than left to be discovered:

  • One admin, no roles. There is exactly one master admin. Everyone who logs in is that admin. Multi-user and RBAC are a different arc.
  • A restart clears the attempt counters and all sessions. Whoever can restart your server is not slowed by the attempt limit.
  • The attempt limit keys on the client IP. An attacker with many source addresses is not stopped by it. The memory-hard KDF — 64 MiB and three passes per attempt — is what makes distributed online guessing expensive; the limiter makes the single-host case pointless.
  • Behind a reverse proxy, the attempt limit is a SHARED bucket. The address the server observes is the proxy's, so every client counts into one counter: five failures from anyone lock out everyone, for fifteen minutes. The engine will not read X-Forwarded-For to get around this, because a header any client can set is a header any client can use to give itself a private bucket — and the same reasoning is what §5 now rests on. The fix is a trusted-proxy policy the operator declares, so the header is read only where an operator has said a proxy exists; that key does not exist yet. Until it does, put the console behind loopback (an SSH tunnel, §4.4) or terminate TLS on the server itself rather than in front of it.
  • CSRF: the ACT routes carry a token, the login form does not. POST /console/act and POST /console/act/preview require a session-bound synchronizer token — an HMAC over your session id under the process's session key, rendered into every act form and verified in constant time. Another origin cannot read it off the page, so a cookie alone is not enough to fire an act. SameSite=Lax on the cookie stays as the outer layer, but it is a browser's promise and invisible to the server, which is why it is not the mechanism. The login form still has no token: it changes no record state, and a cross-site POST to it can at worst log you in as yourself.
  • A restart invalidates every act token, because it invalidates every session. Reload the queue after a restart; a stale form posts a token bound to a session that is gone.
  • This is not browser OAuth. Full OAuth for the console — code + PKCE + callback + logout against an identity provider — is a separate piece of work. This master-admin credential is what that work will sit beside, not something it replaces.
  • /dashboard is not behind this session. It is a different surface with its own history, and it is changed on its own.

Two things this page does not promise

  • There is no terminal SCREEN in the console yet. This is the endpoint. A browser needs a terminal emulator to render what comes out of it, and that is a separate piece of work.

  • The pseudo-terminal itself is unproven on hosts without device nodes. The engine's own test harness is refused /dev/ptmx, so the pty-level suite skips there and says so. To measure it where device nodes exist:

    (run by the product’s own suite)

    That variable turns the skip into a failure, so a host that should have a pseudo-terminal cannot report a quiet green.

    The session sweep of §6 step 4 is the exception: its own controls (a control in the product’s own suite and its siblings) reproduce a descendant in its own process group with setsid/setpgid directly and need no pseudo-terminal, so that mechanism is measured everywhere. So is the timing of the sweep against a shell that has only just been killed: a control in the product’s own suite runs the close sequence forty times over a plain setsid leader, and a control in the product’s own suite forces the case where the state never arrives and requires the bound to be reached and named.

    What still needs /dev/ptmx is the end-to-end shape — a real shell, real job control, Session.Close — which is a control in the product’s own suite and a control in the product’s own suite.

On this page