You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
As an operator exposing HTTP services that run inside sandboxes through openshell forward service, I want a forwarded TCP connection to cost no gateway store writes, so that connection latency stays flat under concurrency instead of queueing on the store's single writer.
Problem Statement
openshell forward service calls CreateSshSession before every forwarded TCP connection and RevokeSshSession after it (crates/openshell-cli/src/run.rs, the per-connection task in service_forward_tcp). On the gateway, handle_forward_tcp already authenticates the caller and authorizes it against the sandbox's workspace on every stream (resolve_and_authorize_sandbox_name) before it looks at the token; the relay to the supervisor carries only the sandbox id and target; the token is never forwarded, audited, or visible to the target service. Revoking it does not close an established connection either; it only blocks a later open with the same token, and the forward never reuses a token.
The session token exists for openshell sandbox ssh, where an external ssh binary launched through ProxyCommand holds nothing but the token and needs a sandbox-bound, expiring, revocable credential that outlives the CLI invocation. Reused once per forwarded TCP connection it adds no authorization, no kill capability and no durable record, and costs two store commits per connection.
#3543 (WAL, synchronous=FULL, relaxed issuance) reduced what each commit costs, but the revocation commit still holds SQLite's writer lock for one fsync, and the next connection's issuance waits behind it. Store-level measurement of the merged design with a 4.5 ms fsync: per-open latency p50 0.13 ms with one connection, 194 ms with 64 simultaneous opens; burst time about 9 ms per connection, serial, so throughput is capped near one connection per fsync regardless of cores. On the 2 vCPU network-disk VM from #3494 that projects to roughly 0.5 s for 32 simultaneous connections and 1.2 s for 64, against 3.3 s and 6 s before #3543. Better, but still linear in concurrent connections, and entirely avoidable on this path.
Impact / Why This Matters
Any client that opens several connections to a forwarded service at once, or opens and closes connections in quick succession (a browser fetching a page's resources, a connection pool warming up, Connection: close clients), pays roughly one fsync of queueing per concurrent connection gateway-wide, since all forwards share the one writer lock. Workarounds today are limiting client concurrency or keeping connections alive longer, neither of which removes the serial section. Reads are unaffected; this is purely the write pair on the connect/close path.
Proposed Design
Authorize target.tcp ForwardTcp streams on the caller's principal alone, which is already established and checked per stream, and stop requiring authorization_token for them; keep the token required for target.ssh, where the ProxyCommand contract depends on it. Keep the per-sandbox concurrent-connection cap as the in-memory counter keyed by sandbox that it is today. The CLI stops minting and revoking a session per connection; a CLI talking to an older gateway that still demands the token gets a one-time Unauthenticated reply and falls back to per-connection tokens for that process, so both version pairings keep working. Result: no store access per forwarded TCP connection, and per-connection latency independent of how many connections are in flight.
Alternative considered: mint one session per forward service process (two writes per forward lifetime instead of per connection). It keeps a per-forward revocation handle but needs the 3-per-token cap lifted for TCP targets and client-side renewal for forwards that outlive the 24-hour session TTL; the per-stream principal check makes the handle redundant for authorization, so the token-less design is proposed.
Acceptance Criteria
A forwarded TCP connection performs no gateway store write; per-connection latency through openshell forward service does not grow with the number of concurrent connections beyond the per-sandbox cap.
openshell sandbox ssh (ProxyCommand, target.ssh) is unchanged and still requires a session token.
A sandbox identity cannot reach any sandbox through ForwardTcp that it could not reach before (per-stream resolve_and_authorize_sandbox_name remains the gate).
Old CLI with new gateway and new CLI with old gateway both work.
Unit tests cover token-less TCP admission, SSH-target rejection without a token, rejection of an unknown supplied token, and the shared per-sandbox cap; architecture and security docs describe the forward's authorization.
Stateless signed short-lived tokens carrying the sandbox id: zero writes, but adds a token format for a credential that lives one TCP connection and gives up explicit revocation; more machinery than removing the redundant check.
Agent Investigation
Code paths verified on current main: handle_forward_tcp authorizes the principal before the token check; validate_ssh_forward_token only confirms existence, non-revocation and sandbox match; bridge_forward_tcp_stream never re-checks the token; handle_revoke_ssh_session only updates the store row; resolve_and_authorize_sandbox_name resolves a sandbox principal only to its own sandbox. A branch implementing the proposed design with the tests above is ready and will be submitted as a PR referencing this issue.
User Story
As an operator exposing HTTP services that run inside sandboxes through
openshell forward service, I want a forwarded TCP connection to cost no gateway store writes, so that connection latency stays flat under concurrency instead of queueing on the store's single writer.Problem Statement
openshell forward servicecallsCreateSshSessionbefore every forwarded TCP connection andRevokeSshSessionafter it (crates/openshell-cli/src/run.rs, the per-connection task inservice_forward_tcp). On the gateway,handle_forward_tcpalready authenticates the caller and authorizes it against the sandbox's workspace on every stream (resolve_and_authorize_sandbox_name) before it looks at the token; the relay to the supervisor carries only the sandbox id and target; the token is never forwarded, audited, or visible to the target service. Revoking it does not close an established connection either; it only blocks a later open with the same token, and the forward never reuses a token.The session token exists for
openshell sandbox ssh, where an externalsshbinary launched through ProxyCommand holds nothing but the token and needs a sandbox-bound, expiring, revocable credential that outlives the CLI invocation. Reused once per forwarded TCP connection it adds no authorization, no kill capability and no durable record, and costs two store commits per connection.#3543 (WAL,
synchronous=FULL, relaxed issuance) reduced what each commit costs, but the revocation commit still holds SQLite's writer lock for onefsync, and the next connection's issuance waits behind it. Store-level measurement of the merged design with a 4.5 msfsync: per-open latency p50 0.13 ms with one connection, 194 ms with 64 simultaneous opens; burst time about 9 ms per connection, serial, so throughput is capped near one connection perfsyncregardless of cores. On the 2 vCPU network-disk VM from #3494 that projects to roughly 0.5 s for 32 simultaneous connections and 1.2 s for 64, against 3.3 s and 6 s before #3543. Better, but still linear in concurrent connections, and entirely avoidable on this path.Impact / Why This Matters
Any client that opens several connections to a forwarded service at once, or opens and closes connections in quick succession (a browser fetching a page's resources, a connection pool warming up,
Connection: closeclients), pays roughly onefsyncof queueing per concurrent connection gateway-wide, since all forwards share the one writer lock. Workarounds today are limiting client concurrency or keeping connections alive longer, neither of which removes the serial section. Reads are unaffected; this is purely the write pair on the connect/close path.Proposed Design
Authorize
target.tcpForwardTcp streams on the caller's principal alone, which is already established and checked per stream, and stop requiringauthorization_tokenfor them; keep the token required fortarget.ssh, where the ProxyCommand contract depends on it. Keep the per-sandbox concurrent-connection cap as the in-memory counter keyed by sandbox that it is today. The CLI stops minting and revoking a session per connection; a CLI talking to an older gateway that still demands the token gets a one-timeUnauthenticatedreply and falls back to per-connection tokens for that process, so both version pairings keep working. Result: no store access per forwarded TCP connection, and per-connection latency independent of how many connections are in flight.Alternative considered: mint one session per
forward serviceprocess (two writes per forward lifetime instead of per connection). It keeps a per-forward revocation handle but needs the 3-per-token cap lifted for TCP targets and client-side renewal for forwards that outlive the 24-hour session TTL; the per-stream principal check makes the handle redundant for authorization, so the token-less design is proposed.Acceptance Criteria
openshell forward servicedoes not grow with the number of concurrent connections beyond the per-sandbox cap.openshell sandbox ssh(ProxyCommand,target.ssh) is unchanged and still requires a session token.resolve_and_authorize_sandbox_nameremains the gate).Alternatives Considered
fsyncis inherent to a durable revocation per connection.Agent Investigation
Code paths verified on current
main:handle_forward_tcpauthorizes the principal before the token check;validate_ssh_forward_tokenonly confirms existence, non-revocation and sandbox match;bridge_forward_tcp_streamnever re-checks the token;handle_revoke_ssh_sessiononly updates the store row;resolve_and_authorize_sandbox_nameresolves a sandbox principal only to its own sandbox. A branch implementing the proposed design with the tests above is ready and will be submitted as a PR referencing this issue.Related: #3494, #3543.