Skip to content
Commands

SSH into a sandbox

kobe ssh-proxy lets any SSH client reach a sandbox: ssh, scp, an editor's remote mode, a terminal that opens many panes on one host. The client never learns Kobe exists. It runs kobe ssh-proxy as its ProxyCommand, which resolves the host name to one of your sandboxes and carries the SSH protocol over kobe attach. Kobe moves the bytes without reading them; the session is encrypted end to end between your machine and the sandbox.

Set up once

kobe init --endpoint https://kobe.example.com

init does every step ssh kobe-… depends on, skipping the ones already done, so running it again is cheap:

  1. target: with --endpoint, writes (or replaces) a target and makes it current for this shell. The auth mode is discovered from the endpoint (ssh when offered, then oidc, then token); --auth overrides it. Without --endpoint, the current target is used.
  2. session: authorization must work without prompts, because that is what ssh-proxy gets. The one-time trust question (ssh auth) or the browser login (oidc) happens here, in your terminal.
  3. default pool: the pool ssh kobe-<name> uses when the host does not name one. Chosen from the pools that can serve SSH; with several, init asks. --default-pool picks one outright.
  4. public key: found under ~/.ssh, remembered with --public-key, or generated on request.
  5. ssh_config: writes Kobe's own file and adds one Include line at the top of ~/.ssh/config. Nothing else in that file is touched.
  6. proof: ssh -G must resolve a kobe-* host to kobe ssh-proxy.

--yes (or no terminal, or --output json) never prompts: a step that would need an answer fails with the command that supplies it.

The block init installs is what kobe ssh-config prints:

# Kobe sandboxes over SSH. Generated by `kobe ssh-config`.
# `ssh kobe-<pool>-<name>` leases the sandbox on first use and reuses it after.
# `ssh kobe-<name>.<session>` logs in to a session that survives disconnects.
Host kobe-*
    User nonroot
    ProxyCommand /opt/homebrew/bin/kobe ssh-proxy %n
    StrictHostKeyChecking no
    UserKnownHostsFile /dev/null
    LogLevel ERROR
    ServerAliveInterval 30
    ServerAliveCountMax 4

The executable is spelled out in full because ssh runs a ProxyCommand without a login shell, where kobe may not be on PATH. The Include goes at the top of ~/.ssh/config because an Include below a Host line applies to that host only.

Host keys are not checked: a sandbox recreated under the same name has a fresh key, and the far end is already authenticated by Kobe, which only ever attaches you to a sandbox you own.

When it does not connect

kobe doctor

doctor runs the same checks read-only and prints one line per check: the binary, the target, the endpoint and its auth methods, whether authorization works without prompts, which pools can serve SSH and which is the default, your active sandboxes and their expiry, the public key, the ssh_config include, and what ssh -G kobe-… resolves to. Each failure names the command that fixes it; most of the time that is kobe init. Exit status is 1 when any check failed. --output json is for pasting into an issue.

Connect

ssh kobe-small-1

The host name selects the pool and names the sandbox:

HostPoolAlias
kobe-small-1smallkobe-small-1
kobe-ci-gpu-2ci-gpukobe-ci-gpu-2
kobe-devthe target's default poolkobe-dev

The pool is the longest pool name the host starts with, so ci and ci-gpu are both addressable. A name that matches no pool uses the target's default pool, set with kobe init --default-pool <pool>. With neither, the connection fails and lists the pools you can use.

The whole host name is the lease alias. kobe-small-1 and kobe-small-2 are two sandboxes; a teammate's kobe-small-1 is a third, because aliases are scoped to the caller. The alias is what kobe status, kobe extend and kobe release see:

kobe release kobe-small-1

First connection creates the sandbox

If no active sandbox carries the alias, ssh-proxy leases one from the pool, waits until it is ready, and reports on stderr, which ssh shows you:

kobe: creating kobe-small-1 in pool small...
kobe: kobe-small-1 is ready (sandbox-a1b2c3d4e5f60718293a4b5c); release with `kobe release kobe-small-1`

Later connections find the alias and reuse it. A pre-warmed pool answers in seconds; a cold pool may take longer, and --wait-timeout bounds the wait (default five minutes). ssh has no timeout of its own for a ProxyCommand.

Disconnecting releases nothing. The sandbox lives until its TTL runs out or you release it. A sandbox that expired is gone: the next ssh kobe-small-1 creates a new, empty one with the same name.

Sessions that survive a disconnect

A plain login shell ends with its connection. Add .<session> to the host and the login lands in a persistent session instead:

ssh kobe-dev.main    # sandbox kobe-dev, session "main", created on first use
ssh kobe-dev.build   # the same sandbox, another session

The shell runs under kobe-runner in the sandbox, not under sshd, so a dropped connection leaves it running. Run the same ssh again and the screen is redrawn as you left it. The session ends when its shell exits.

Only interactive logins go to the session. A command (ssh kobe-dev.main make), scp, sftp and an editor's remote server run exactly as they would on kobe-dev. The session name comes after the first dot and uses lowercase letters, digits and -.

ssh does not reconnect by itself. For a terminal that does, use kobe attach kobe-dev.main.

Your key is authorized on every connection

Before attaching, ssh-proxy appends your public key to the sandbox's authorized_keys. It looks, in order, at --public-key, ssh_public_key in the kobe config, then ~/.ssh/id_ed25519.pub, id_ecdsa.pub, id_rsa.pub. The key travels on the execution's stdin, never in argv, so it does not land in the target cluster's audit log. This uses kobe exec, so the pool must declare runnerPath.

Copying files

Everything that rides SSH rides this: scp, sftp and rsync all work against a kobe-* host with no extra setup, because they read the same ssh_config block kobe init wrote.

scp ./overlay.tar kobe-sandbox-dev:/tmp/
scp kobe-sandbox-dev:/tmp/results.json .
rsync -az ./src/ kobe-sandbox-dev:/home/agent/work/src/

The sandbox is created on the first connection, so these work from cold.

A transfer is not an execution. It travels on the same stream as the SSH session, so it does not count against the per-lease execution limit and cannot exhaust it. That matters because running out of executions leaves a running job unreachable: no logs, no cancel.

This is the answer to moving a file rather than kobe exec, which converts its output to text and corrupts anything binary on the way back.

Sessions and the operation limit

Kobe admits at most eight simultaneous operations per sandbox. One SSH connection is one operation, no matter how many sessions the client multiplexes over it: ssh with ControlMaster, or a terminal that opens many panes on one host, stays at one. Opening a separate ssh per pane costs one operation each.

What the pool needs

The zondax/kobe-sandbox images ship kobe-sshd, which serves one session over the attach stream as the workload user. No port is opened, so exposedPorts is not involved. A custom image needs openssh-server, a kobe-sshd launcher, and an account that sshd will let in; see the image's Dockerfile for the working shape.

Flags

kobe ssh-proxy <host> [--pool <pool>] [--ttl <duration>] [--wait-timeout <duration>]
                      [--no-create] [--public-key <path>]
FlagDescription
--poolPool for a new sandbox. Overrides the host name and the default pool.
--ttlTTL for a new sandbox. Pool default when omitted.
--wait-timeoutBound on waiting for a new sandbox. Default 5m.
--no-createConnect to an existing sandbox only.
--public-keyPublic key file to authorize.

Pass flags through ProxyCommand, for example ProxyCommand kobe ssh-proxy --ttl 4h %n.

ssh-proxy refuses to run with a terminal on stdout, because its stdout is the SSH transport. Run ssh kobe-<pool>-<name> instead.

Available for:
Apple macOS logomacOSMicrosoft Windows logoWindowsLinux logoLinux
Download Kunobi