Skip to content

CLI: ssh connection sharing in the user's ssh_config sends one sandbox's session into another #3935

Description

@vineethsai7

Summary

The openshell CLI opens its ssh sessions (sandbox connect, sandbox upload,
sandbox download, forward start) by running ssh from PATH with a
ProxyCommand that names the sandbox and a session token, and the literal host
name sandbox for every sandbox. The user's ~/.ssh/config and
/etc/ssh/ssh_config still apply, and the CLI does not turn off OpenSSH
connection sharing. With a common multiplexing setup such as

Host *
  ControlMaster auto
  ControlPersist 10m
  ControlPath ~/.ssh/cm-%C

every sandbox resolves to the same control socket (%C hashes the local host,
the host name sandbox, the port and the user, which are the same for every
sandbox). The first session becomes the master and stays up for
ControlPersist; every later ssh the CLI starts, whatever sandbox it names,
connects to that master instead of running its own ProxyCommand, and so
reaches the first sandbox.

Observed on one Mac with two sandboxes, A and B, on one gateway:

  • openshell sandbox upload A … followed, within 10 minutes, by
    openshell sandbox upload B … put B's files into A. The gateway logged a
    CreateSshSession for the second upload but no ForwardTcp: the session
    token for B was created and never used.
  • openshell forward start --background <port> B failed with
    ssh exited before local forward listener opened.

By the same mechanism openshell sandbox connect B can attach the terminal to
A (not measured separately), which is the worst case: the user types into a
sandbox other than the one they named.

The effect is silent: no error, and the data lands in the wrong sandbox.

Environment

  • OpenShell CLI 0.1.1, local gateway, two sandboxes on the MicroVM (vm)
    driver; the CLI code path is driver-independent, so the docker driver and
    Linux are affected the same way.
  • macOS 27.0 on Apple silicon, /usr/bin/ssh (OpenSSH_10.3p1, LibreSSL 3.3.6).
  • ~/.ssh/config with Host *, ControlMaster auto, ControlPersist 10m,
    ControlPath ~/.ssh/cm-%C.

Steps to reproduce

  1. Put the Host * block above in ~/.ssh/config.

  2. Create two sandboxes, a and b, on the same gateway.

  3. Confirm they share a control path:

    $ ssh -G sandbox | grep -i ^control
    controlmaster auto
    controlpath /Users/me/.ssh/cm-<hash>
    controlpersist 600
    
  4. Upload to each within ten minutes:

    $ mkdir -p /tmp/up-a /tmp/up-b && touch /tmp/up-a/from-a /tmp/up-b/from-b
    $ openshell sandbox upload a /tmp/up-a /sandbox
    $ openshell sandbox upload b /tmp/up-b /sandbox
    $ openshell sandbox exec -n a --no-tty -- ls /sandbox/up-a /sandbox/up-b
    $ openshell sandbox exec -n b --no-tty -- ls /sandbox/up-b
    

    Expected: up-b in b only. Observed: up-b is in a, and not in b.

  5. With the master from step 4 still up, openshell forward start --background 18080 b fails with ssh exited before local forward listener opened.

A PATH-first ssh wrapper that runs /usr/bin/ssh -o ControlMaster=no -o ControlPath=none "$@" makes both steps work (each upload lands in the
sandbox it names, and the forward starts), which isolates the cause to ssh
connection sharing.

Expected

Every openshell ssh session reaches the sandbox it names, whatever the user's
ssh configuration says about connection sharing.

Cause

The CLI builds its ssh command lines with -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o GlobalKnownHostsFile=/dev/null -o LogLevel=ERROR (plus -o ConnectTimeout=15 -N -f -L … for forwards, and
-tt -o SetEnv=TERM=… for connect), a ProxyCommand of
openshell ssh-proxy --gateway <url> --sandbox <name> --token <token>, and the
host sandbox, but no ControlMaster, ControlPath or ControlPersist
options, so ssh takes them from the user's configuration. Because the host name
is the same for every sandbox, so is the expanded ControlPath, and ssh reuses
a master whose ProxyCommand named another sandbox. (A master's session token
is also only good for the sandbox it was issued for, so sharing is never
correct across sandboxes.)

Suggested fix

Pass connection-sharing options on every ssh command line the CLI builds; on
the command line they take precedence over every ssh_config file:

-o ControlMaster=no -o ControlPath=none -o ControlPersist=no

If the CLI wants multiplexing for speed, it should own it: a ControlPath in
the CLI's own state directory (owner-only) whose name includes the gateway,
workspace and sandbox (or a per-sandbox HostKeyAlias / host name such as
openshell-<gateway>-<workspace>-<sandbox>, so %C differs per sandbox), and
never the user's. The per-session token argues for simply turning sharing off.

The same applies to the ssh config that sandbox connect --editor installs,
if its host names are not already unique per sandbox.

Workaround used downstream

DefenseClaw runs every openshell command with a private directory (0700) first
on the child's PATH holding an ssh shim:

#!/bin/sh
exec /usr/bin/ssh -o ControlMaster=no -o ControlPath=none -o ControlPersist=no "$@"

(the real ssh resolved once from the original PATH), without changing the
user's ssh configuration. Its doctor checks ssh -G sandbox through the shim
and warns users whose own configuration shares connections for sandbox,
since openshell commands they run themselves are still affected. Users can
also add, above Host *:

Host sandbox
  ControlMaster no
  ControlPath none

🤖 Generated with Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    state:triage-neededOpened without agent diagnostics and needs triage

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions