SSH and herdr
isb serve carries SSH over its own authenticated websocket, so plain
ssh, scp, rsync, editors' remote modes and herdr machine add reach
any instance of an org you may exec into: its workspace, a sandbox, a
replica. Nothing listens in the instance and no port is opened on the host or
anywhere else. The keys that get in are the SSH public keys on your isb
account, so removing a key from your account locks it out everywhere at once.
isb key add ~/.ssh/id_ed25519.pub # once: the key goes on your isb account
isb ssh-config acme/box -o ~/.config/isb/ssh_config # then `Include` it from ~/.ssh/config
ssh box.acme.isb
scp build.tar box.acme.isb:/tmp/
herdr machine add box.acme.isb --label acme/box
For an org's workspace, isb workspace ssh-config writes the block for it
(it is isb ssh-config for the workspace's instance), and the web UI's
workspace Connect tab shows the same lines.
How it works#
ssh runs isb ssh-proxy ORG/INSTANCE as its ProxyCommand. That opens
GET /orgs/<org>/api/v1/ssh?instance=NAME on isb serve, a websocket whose
binary frames are the SSH connection's bytes. The daemon starts
sshd -i inside the instance through incus exec, as root, and bridges the
two. sshd does the SSH: its host key, your login, your shell, port
forwarding, scp and sftp.
Why sshd -i through exec rather than a TCP connection to the instance's
port 22:
- Nothing listens. The instance needs OpenSSH's server installed
(
openssh-server), not running. No address, ACL, egress rule or firewall is involved, so a VM, an org without egress and an instance whose network is down all work the same. - Your keys, per connection. For each connection the daemon writes the
account's keys at that moment to a file under
/run/isb-ssh/and tells sshd to read only that file (AuthorizedKeysFile, withAuthorizedKeysCommand, principals and passwords off). A key removed from the account never opens another session, nothing has to be synced into the instance, and the instance's own~/.ssh/authorized_keysgrants nothing here. The file is only needed while the client authenticates; it is removed two minutes later. - sshd says who. Its log (on stderr, never on the SSH stream) names the guest user and the key that authenticated. The audit row records both, and the key's last use shows on the Account page.
The guest user is yours to pick (User in the config, ssh user@...):
anyone admitted may already exec as root in the org's instances, so SSH
adds no reach.
For an org placed on a server, the control plane bridges the websocket to
that server's agent, sending the caller's keys as it reads them, and checks
the session here as for a local one; the agent runs sshd -i in the
instance (SSH to an org on a server).
Who gets in#
The websocket is admitted exactly like the web terminal: as if calling
sandbox_exec in the org (The web
terminal). Members, admins and
owners of the org and platform admins get in; viewers, tokens whose scopes
leave out sandbox_exec (read, deploy) and everyone when --deny-tools
covers sandbox_exec do not. A cookie needs an Origin naming the site; a
bearer token and the unix socket need none.
Then the SSH layer: only the keys on the caller's own isb account. The unix
socket and superadmin tokens have no account of their own, so they name one
with --as EMAIL (as= on the websocket); anyone else may only name
themselves.
A live session ends within 15 seconds of any of: its key removed from
the account, the account disabled, the API token that opened it revoked or
expired, the browser session that opened it ending, the user leaving the
org or becoming a viewer in it. The client sees isb: the SSH key that opened this session (SHA256:...) was removed from the account.
Limits: 64 SSH sessions per daemon, closed after 2 hours without a byte either way (the generated config sends a keepalive every 30 seconds) and after 24 hours in all.
Every session is in the audit log: ssh.open, and ssh.close with the guest
user, the key's fingerprint and how long it ran (Audit
log).
Keys#
isb key add FILE (- for stdin) [--name N] |
add a public key (.pub); its comment is the default name |
isb key ls [--json] |
id, name, type, fingerprint, last use |
isb key rm ID... |
remove; sessions it opened end within seconds |
| Account page, SSH keys | the same, in the web UI |
GET/POST /api/v1/auth/ssh-keys, DELETE /api/v1/auth/ssh-keys/<id> |
the API ({"public_key", "name"}) |
With --url (or ISB_URL) and a token (ISB_TOKEN, or --token-file),
the commands act on the token's own account. Without, they open the
identity store on the host, like isb token create, for --user EMAIL
(default: the only platform admin).
Accepted: ssh-ed25519, ecdsa-sha2-nistp256/384/521, the sk- (security
key) variants, and ssh-rsa of 2048 bits or more. A key is stored as its
type and data only: authorized_keys options in front of it (command=,
from=) are refused, not dropped, and its comment never reaches an
instance. At most 50 keys per account. Adding and removing a key is in the
audit log (auth.ssh_key_add with the fingerprint, auth.ssh_key_remove).
An API token scoped short of admin cannot change keys.
isb ssh-config#
isb ssh-config [ORG/INSTANCE ...] [--user U] [--identity FILE] [--known-hosts FILE]
[--as EMAIL] [--isb PATH] [-o FILE] [--url URL] [--token-file FILE]
Prints a Host block per instance (none named: every running instance in
--org):
Host box.acme.isb
HostName box.acme.isb
User dev
ProxyCommand /usr/local/bin/isb ssh-proxy acme/box --url https://isb.example.com --token-file /home/me/.config/isb/token
HostKeyAlias box.acme.isb
UserKnownHostsFile /home/me/.config/isb/known_hosts
StrictHostKeyChecking yes
ServerAliveInterval 30
ServerAliveCountMax 4
- Host keys are pinned, not trusted on first use.
ssh_host_keys(a read-only tool) reads the instance's/etc/ssh/ssh_host_*_key.pubthrough incus, andisb ssh-configwrites them to its own known_hosts file (default~/.config/isb/known_hosts) under the block'sHostKeyAlias, replacing that alias's old lines and keeping every other line. Run it again after rebuilding an instance. An instance with no host keys (OpenSSH installed, never started) getsStrictHostKeyChecking accept-new; its first connection generates them (ssh-keygen -A). - User:
--user, else the instance's first ordinary user (uid 1000 and up, with a login shell), else root. - Where isb is:
--url/ISB_URLputs--urlin theProxyCommand. The token is read from--token-file(passed through) or fromISB_TOKENwhere ssh runs, never written into the config. Without a URL the proxy uses the local daemon's unix socket, with--as(default: the only platform admin). -o FILEwrites the blocks to a file; addInclude FILEnear the top of~/.ssh/config.
isb ssh-proxy ORG/INSTANCE [--as EMAIL] [--url URL] [--token-file FILE]
is the ProxyCommand itself: stdin and stdout are the SSH connection.
When the daemon refuses (not allowed, no keys, no sshd in the instance), it
prints why on stderr and exits 1, and ssh reports the connection closed.
herdr#
herdr, the terminal multiplexer for coding agents, reads ~/.ssh/config, so
once the Include is there, herdr machine add box.acme.isb works like any
SSH machine: its panes and agents run in the instance. isb ssh-config
prints the herdr line for each instance on stderr, and the workspace's
Connect tab shows it for the workspace.
Limits#
isb ssh-proxysends only the isb token, no Cloudflare Access credentials, so it cannot pass a daemon behind Access. Reach the daemon on a tailnet or loopback--listenaddress instead (Reach isb serve remotely).- The web terminal's shells outlive their tab only in a workspace with herdr installed, where each tab is a herdr session (The web terminal); elsewhere, closing a page or a tab ends its shell. Use SSH (and herdr) for sessions on other instances.