Skip to content

SSH & the front door

The front door is a plain SSH listener where the username is a box spec and the client key is the identity — no signup, no pre-created box. It's how humans reach boxes; anything that speaks SSH works (ssh, VS Code Remote-SSH, scp, rsync, JetBrains Gateway), with no public port on the box.

sh
ssh proj@host                  # spawn/attach box "proj" (default image)
ssh proj:ubuntu-22.04@host     # pick a catalog image
ssh proj:debian-12:cpu+5m@host # image + flavor, stay alive 5m after disconnect
ssh proj+@host                 # force a fresh box
ssh images@host                # list the image catalog (spawns no box)
ssh cli@host ls                # the management CLI (spawns no box)

How it works

The box's agent dials out to hopboxd and serves an SSH server over that reverse tunnel — nothing routes into the box. The front door authenticates you by key, then proxies your session straight into the box's SSH server, so shells, exec, and the sftp subsystem (scp/sftp/rsync) all work end-to-end.

Port forwarding

Local forwarding (ssh -L) works — tunnel a port on a box back to your machine:

sh
ssh -L 8080:127.0.0.1:8080 proj@box.hopbox.dev   # reach the box's :8080 at localhost:8080

The box dials the target from inside itself, so a forward reaches only what the box can reach — its own loopback and whatever its egress allows. That is the box's existing network fence doing its job, not a separate forwarding policy; you cannot use a forward to pivot to something the box couldn't already talk to. A forward to a closed port fails with the target named (direct-tcpip: dial …: connection refused) and leaves the session intact.

This is what VS Code Remote-SSH, JetBrains Gateway, and dev-server / database tunnels use. Reverse forwarding (ssh -R) is not supported yet.

For an HTTP service, you may not need a tunnel at all. The API can proxy a box port directly — GET /v1/boxes/{box}/proxy/{port}/…, authed by your hbx_ key — so a browser can open the app a box is serving with nothing to set up. See the HTTP API. Reach for ssh -L when the thing on the port isn't HTTP (a database, a raw TCP service) or when you want it on your own machine's localhost.

Username grammar

[workspace/]name[:image[:flavor]][^ttl][+duration]
SegmentMeaning
workspace/Optional workspace prefix. Omit = your default workspace (unchanged behavior); acme/web is box web in workspace acme, with its own /wrk drive, home, and secrets.
nameBox name, created on first connect. Names are per-owner, and per-workspace — your proj is distinct from another key's proj, and acme/proj is distinct from your default proj. Append + to force a fresh box.
:imageBox image. With docker, any OCI ref. With microVM, a catalog name (ssh images@host lists them). Omit = --default-image.
:flavorHardware flavor. A recognized named flavor sets the box's CPU/memory caps, overriding the front-door defaults.
^ttlHard deadline from creation (30m, 2h). The box is torn down then, whatever is happening — an attached session, a keep-alive pin and the durable flag do not extend it. Clamped to your tier's ceiling. Omit = no deadline (a run box gets its tier's default).
+durationStay-alive grace after disconnect (5m, 1h). Omit = the daemon default grace.

^ttl is written before +duration when both are used — proj:go^30m+5m is a box that keeps running 5 minutes after you disconnect and is destroyed 30 minutes after it started. ^ rather than ! because an interactive bash or zsh would history-expand a ! inside an ssh argument.

A deadline is what makes a box safe to walk away from: a build that wedges or an agent that loops stops costing you something at a time you chose, rather than at idle-reap hours later. It is enforced by the daemon, so it survives whatever started the box crashing.

Some usernames are reserved:

  • images (or image) — prints the image catalog (spawns no box).
  • cli — your management CLI (ls, rm, …), key-scoped (no box).
  • sudo — the fleet-wide admin console (all owners); allowed only for keys in the daemon's --admin-keys file (no box).
  • _ — an anonymous throwaway box: a fresh one each connect, no stable name.
  • session-<id>reconnect to one of your boxes by id (or the id-prefix shown by ssh cli@host ls).
  • jump — the confidential relay: ssh -J jump@host root@<box> reaches a box end-to-end, with the gateway relaying ciphertext only (no box).
  • hbxs_<token> — a share link: connects the holder to someone else's box (the one the token grants), with any key. Minted by the box owner with ssh cli@host share <box>.

Lifetime — auto-suspend

Front-door boxes auto-suspend when idle: after --idle-timeout (default 2m) with nobody attached and no shell open in the box, it is snapshotted to disk (compute stops) and your next connection resumes it — nothing is lost on disconnect, you reconnect straight back in. A box never reconnected to is eventually idle-reaped (--idle-reap, default 3h); box-guest auto-suspend off or a keep-alive pin holds it longer. See lifecycle.

Identity

Your SSH key is your identity; the box is owned by that key's fingerprint. Reconnecting with the same key reuses the same box (within its lifetime); a different key connecting to the same name is refused while that box is alive. See Identity & tiers.

Confidential access — ssh -J

By default the front door terminates your SSH: it authenticates you, opens its own session into the box, and proxies it — so the gateway can see the session. For work where the gateway must not be able to read the session, reach the box end-to-end with a jump:

sh
ssh -J jump@box.hopbox.dev root@mybox            # end-to-end; the gateway only relays
scp -J jump@box.hopbox.dev ./f root@mybox:/tmp/  # scp/sftp ride the same relay

The reserved jump front-door user puts the connection in relay mode: the gateway forwards raw bytes to the box's SSH server and never opens a session of its own — it holds no key to the box and sees only ciphertext. The box authenticates your key directly (the gateway hands it your public key at spawn; your private key and the session never leave your client).

A box first reached this way is confidential for its lifetime: it authenticates you and refuses the normal terminated pathssh mybox@host returns a pointer to use ssh -J. The jump target is a box spec (root@mybox, or root@mybox:python+1h).

Reference

Instant isolated compute — for humans and AIs