SSH starts disabled. The Rust runtime exposes LiteBox::ssh() to configure,
query, or disable it, for local boxes and for REST-backed boxes served by
boxlite serve (HTTP control) or the
hosted cloud API. Only
configure() follows the implicit-start policy of exec, metrics, and file
copying. A Configured or Stopped box using the image's default command can
start implicitly. If BoxOptions.cmd or entrypoint explicitly sets the main
command, configuration returns InvalidState in these states; call start()
first. On a REST server, a stopped box with auto_resume=false is refused
instead of resumed.
status() and disable() never start the VM or container main command. Valid
Configured or Stopped handles return disabled status with generation zero and
empty address and host identity fields, regardless of the configured command.
Running boxes send requests to the existing guest, including after attach()
before start() and after runtime recovery. Other lifecycle states return
InvalidState. Results describe the state observed by the call; a concurrent
configure() can subsequently enable SSH.
Creating the handle alone does not start anything. Deployments without SSH
control answer 404 or reject with Unsupported; there is no capability
negotiation. See the SDK API references
for language-specific control APIs.
SSH does not publish a host port; forward one with boxlite ssh forward
or other network forwarding. For component diagrams and implementation details, see
Guest SSH architecture.
use boxlite::{SshAccount, SshConfig};
let ssh = sandbox.ssh();
let status = ssh.configure(SshConfig {
listen_address: "0.0.0.0:2222".into(),
host_private_key: std::fs::read_to_string("host_key")?,
accounts: vec![SshAccount {
login: "alice".into(),
authorized_keys: vec![std::fs::read_to_string("alice.pub")?],
ca: None,
}],
}).await?;
println!("{} {}", status.host_public_key, status.host_key_fingerprint);
let current = ssh.status().await?;
ssh.disable().await?;SshHandle owns its backend reference and can outlive the LiteBox borrow.
A fresh handle to a running VM can query SSH without calling start() again.
For a local box, after startup each operation has a total 15-second budget:
the guest's 10-second cleanup limit plus 5 seconds for communication and
scheduling. Obtaining the SSH interface, connection setup, queueing, and the
RPC all consume this budget; VM and container startup time is excluded.
Connection or queueing delays can leave less than 10 seconds for guest cleanup.
Local runtime shutdown rejects new SSH operations and cancels interface
acquisition and RPCs. In-progress VM initialization finishes before
cancellation is returned, so detached boxes remain recoverable; container
startup retains its own cancellation. Operations are not automatically retried.
Timeout or cancellation does not undo changes the guest may already have
applied.
Invalidated handles return Stopped; drop all references to the old box and
obtain a fresh handle with runtime.get(). That handle can query or disable SSH
without restarting the box. A stopped box with an explicit main command still
requires start() before configure().
boxlite ssh wraps the same handle for local and REST boxes; the
CLI reference lists every flag.
boxlite ssh configure dev --ssh-config ssh.json # complete SshConfig; prints SshStatus
boxlite ssh forward dev # forwards 127.0.0.1:<random> to guest SSH
boxlite ssh disable devConfigure SSH first, then use forward to open a local listener until Ctrl-C.
For one account, configuration can instead read key files with
boxlite ssh configure dev --host-key host_key --login alice --authorized-key alice.pub.
configurereplaces the whole configuration once, which disconnects every SSH client, including those connected through another tunnel.forwardreads status and targets the port SSH actually bound, so a guest listener on port0works. It leaves configuration and generation unchanged. Ctrl-C ends only local forwarding; guest SSH stays enabled.- SSH lives in guest memory. A local CLI exit stops boxes that are not detached
(plain
boxlite run, SDK boxes withdetach=false); a box such a command started loses SSH with its VM. Boxes fromboxlite createandrun -dkeep running. - Over REST, forwarding opens the box's public tunnel for the SSH port and leaves it
open, so anyone who reaches it can attempt a login and wake a stopped box; SSH keys
remain the access control. Close it with
boxlite network tunnel BOX PORT --revoke.
The local server exposes the same handle at /v1/boxes/{box_id}/ssh. Against
another compatible server the Rust client uses the existing path_prefix
convention, /{prefix}/boxes/{box_id}/ssh:
| Method | Behavior |
|---|---|
POST |
Replace the complete SshConfig body; returns the resulting SshStatus. Every accepted call fully restarts the guest listener, so never retry it automatically. |
GET |
Read SshStatus. Never starts the VM or its main command. |
DELETE |
Disable SSH idempotently; returns SshStatus. Never starts the VM or its main command. |
The lifecycle and resume rules above also apply over HTTP. Authenticated
POST requests count as AutoStop activity, including rejected configurations;
GET, HEAD, and DELETE do not.
Send JSON with the fields defined in the OpenAPI contract.
Unknown fields are rejected in the configuration, accounts, and CA objects.
Malformed bodies return 400 invalid_argument with invalid SSH configuration,
without echoing request values. Invalid configurations leave the running
listener untouched. Other errors include 404 not_found, 409 invalid_state
for lifecycle conflicts (409 conflict for AutoResume refusal), and
503 upstream_unavailable for guest RPC failures.
The Rust client's 300-second request timeout includes startup; a timeout does not undo configuration or stop work still running on the server. See the Rust SSH reference for types and API details.
Build the request body from local key files; do not commit it or paste a real private key into a document:
# ssh-config.json carries the SshConfig shape, built locally from your keys.
curl -fsS -X POST "$BOXLITE_URL/v1/boxes/$BOX_ID/ssh" \
-H "Authorization: Bearer $BOXLITE_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary @ssh-config.jsonThe hosted API serves the same three operations at /v1/boxes/{box_id}/ssh and
/v1/{prefix}/boxes/{box_id}/ssh, by box ID or name, within the caller's
organization. The API checks policy and lifecycle; the box's runner validates the
body and calls the guest once.
| Method | Permission | Activity | Lifecycle |
|---|---|---|---|
POST |
write:boxes |
Counts after authorization and box lookup, including lifecycle refusals | Running box: configured. stopped box: auto-resumed on its default command. 409 when the box is not ready and auto_resume=false, when it must start a box with an explicit cmd/entrypoint, or when it cannot resume (error, archived, …). |
GET |
membership | None | started or stopped only; a stopped box reports disabled status with generation 0. Other states answer 409. |
DELETE |
write:boxes |
None | Same as GET; idempotent. |
With auto-resume enabled, configuring SSH waits for a box already starting
(creating, starting, restoring), even with an explicit cmd or entrypoint.
You can request start, then configure SSH while that start completes.
Errors follow the HTTP control rules, with two differences:
- The API parses JSON before proxying. Malformed JSON returns
400Invalid JSON in request body. Repeated keys collapse to their last value before the runner sees them; case-variant and unknown fields, andnullvalues where the schema requires a string or array, are still rejected. The optionalcamay benull. - Lifecycle refusals from the API are a plain
409with a message naming the reason; SDKs read them, like the runner's409 invalid_state, asInvalidState.
SSH lives in guest memory, so every stop, restart, or auto-stop disables it.
After the box runs again, run boxlite ssh configure to restore its configuration,
then boxlite ssh forward to reconnect; forwarding fails on a disabled listener.
Forwarding uses the box's public tunnel, which needs inbound access enabled on the box. Ending
the local boxlite ssh forward leaves that tunnel open; revoke it with boxlite network tunnel BOX PORT --revoke.
The internal host-only boxlite.v1.Ssh gRPC service remains available on
sockets/box.sock after Guest.Init succeeds. It exposes Configure, Status,
and Disable; its schema is in src/shared/proto/boxlite/v1/service.proto.
Raw protocol callers must regenerate their bindings and send
SshConfigureRequest.config using field 4. Legacy string fields listen_address,
ca_public_key, and principal (fields 1–3) are reserved and ignored. A
legacy-only request returns InvalidArgument because config is missing,
without changing the current service. There is no legacy request conversion or
protocol version negotiation.
Configure accepts an unencrypted OpenSSH host private key and a non-empty
accounts list. Each account has a unique login and at least one public key or
CA configuration. Logins are 1–128 ASCII letters, digits, dots, underscores, or
hyphens. Authentication selects only that login's credentials. A CA contains its
public key and the required certificate principal; the principal need not equal
the login. Certificate validity and permissions are checked as before; plain
authorized keys allow PTY and forwarding. Each authorized_keys entry is one
OpenSSH public key with an optional comment, without authorized_keys options.
The former global SshConfig.ca and SshConfig.authorized_keys fields (3 and 4)
are reserved, including their names. Accounts use field 5. Regenerate bindings
and migrate credentials into explicitly named accounts. An old configuration
decodes without accounts and returns InvalidArgument, leaving the existing
service running. There is no implicit root account.
Authentication accounts do not need entries in the container's /etc/passwd.
For example, alice and bob can both execute as the container's default app
user. Shell, exec, PTY, SFTP, and both directions of Unix socket forwarding
inherit the UID/GID selected at container creation from the image USER or box
user override. Accounts share that identity and its file permissions; they do
not provide operating-system isolation from one another.
After entering the container, SSH resolves the actual execution UID and sets
USER, LOGNAME, HOME, and SHELL. Client environment requests cannot override
these values or select an execution identity. A numeric UID without a passwd
entry uses its numeric name, / as home, and /bin/sh as shell. An empty passwd
shell also uses /bin/sh; a configured shell that is missing fails explicitly.
Shell and SFTP start in the user's home, falling back to / if it cannot be
entered. Non-root SFTP and Unix socket helpers clear effective, permitted,
inheritable, and ambient capabilities before serving; they retain the bounding
set. TCP forwarding remains guest-side and uses the session's authorization.
Every valid Configure fully restarts SSH, even if the configuration is identical:
validate → stop listener → disconnect all clients → wait for SSH execution and
forwarding cleanup → bind new listener. Invalid configuration returns a sanitized
InvalidArgument error and leaves the old service running. If stopping takes
longer than ten seconds, the request returns DeadlineExceeded, which the Rust
runtime preserves as BoxliteError::Rpc with the original gRPC status; cleanup remains
tracked and a later Configure must finish draining it before starting another
listener. A bind failure returns Unavailable and leaves SSH disabled, without
restoring the old configuration. Disable performs the same drain and is idempotent.
Status contains only enabled, the actual bound listen_address, generation,
a comment-free host_public_key, and its SHA-256 host_key_fingerprint. Generation
increments on each successful listener start. Disabled status has empty address
and host identity fields. No authentication configuration or private key is returned.
Configuration and status live only in guest memory. Reconnecting to the same VM preserves them. A VM restart starts disabled with generation zero and requires Configure again. Create, clone, export, and import do not carry SSH configuration or state. Old persisted SSH fields are ignored; existing databases and archives are not migrated or rewritten by this change.
SSH restart terminates only SSH work. The container's main process and non-SSH executions continue. Guest shutdown calls Disable before cleaning up executions and containers, and continues cleanup even if Disable fails. SSH does not track guest shutdown: concurrent Configure may briefly restart it, with VM shutdown reclaiming any remaining resources.