Locking it

A token, Cloudflare Access, and what happens with neither.

There are two independent gates. Turn on one or both.

st                                                       # a fresh token every time it starts
SOPHONZ_ACCESS_TEAM=<team> SOPHONZ_ACCESS_AUD=<tag> st   # and Cloudflare Access on top

st always turns the token on. The agent it starts takes --token <secret> (or SOPHONZ_TOKEN), --access-team and --access-aud (or SOPHONZ_ACCESS_TEAM and SOPHONZ_ACCESS_AUD), and reads those variables from the environment st was started in. With neither, anything that reaches the port gets a shell, and its startup banner says so; st never starts it that way.

The shared token

A /?k=<token> link authenticates once and is immediately exchanged for a cookie, so the secret stops travelling in URLs — and out of them into logs. Any other address without a cookie gets a login form rather than a bare 401. Scripts and the websocket still get a plain 401.

The cookie's signing key is derived from the token, so restarting the server does not log your phone out. Repeated wrong guesses from one address are throttled, doubling up to five minutes.

A cookie from the token lasts 30 days. One from a machine link lasts 12 hours: the link is the way back in, and it checks your account each time. sign out — on the sessions page, or at the bottom of the sessions sheet in a terminal — clears the cookie in that browser. Signing out on sophonz.io cannot reach it; each machine holds its own.

Cloudflare Access

SOPHONZ_ACCESS_TEAM=<team>   # <team>.cloudflareaccess.com   (--access-team)
SOPHONZ_ACCESS_AUD=<tag>     # the Access application's AUD tag   (--access-aud)

The tunnel's own identity layer. Access injects a signed JWT on every request it proxies and the agent verifies it, which means a leaked tunnel hostname is not enough to reach a shell.

ttyd stays on loopback

ttyd runs with --url-arg so one process serves every session rather than one process per session. That also means whoever reaches ttyd could pass arbitrary arguments to tmux — which is why ttyd is bound to loopback, runs with --check-origin, and never sees a session name the proxy has not matched against [A-Za-z0-9_-]{1,64}.