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:
- target: with
--endpoint, writes (or replaces) a target and makes it current for this shell. The auth mode is discovered from the endpoint (sshwhen offered, thenoidc, thentoken);--authoverrides it. Without--endpoint, the current target is used. - session: authorization must work without prompts, because that is
what
ssh-proxygets. The one-time trust question (ssh auth) or the browser login (oidc) happens here, in your terminal. - 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,initasks.--default-poolpicks one outright. - public key: found under
~/.ssh, remembered with--public-key, or generated on request. - ssh_config: writes Kobe's own file and adds one
Includeline at the top of~/.ssh/config. Nothing else in that file is touched. - proof:
ssh -Gmust resolve akobe-*host tokobe 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:
| Host | Pool | Alias |
|---|---|---|
kobe-small-1 | small | kobe-small-1 |
kobe-ci-gpu-2 | ci-gpu | kobe-ci-gpu-2 |
kobe-dev | the target's default pool | kobe-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>]
| Flag | Description |
|---|---|
--pool | Pool for a new sandbox. Overrides the host name and the default pool. |
--ttl | TTL for a new sandbox. Pool default when omitted. |
--wait-timeout | Bound on waiting for a new sandbox. Default 5m. |
--no-create | Connect to an existing sandbox only. |
--public-key | Public 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.