# SSH

Log in to a running sandbox as root, at a port of its own.

An SSH connection goes from your machine straight to the server the sandbox is on, not through the API. Whether a sandbox can be reached, and who may log in to it, are two things, and each has calls of its own.

## Turn it on

`PUT /v1/sandboxes/{id}/ssh`

```shell
curl -X PUT https://agental.io/v1/sandboxes/{id}/ssh \
  -H "Authorization: Bearer $AGENTAL_API_KEY"
```

```json
{
  "enabled": true,
  "enabledAt": "2026-10-13T09:02:11.480Z",
  "address": "203.0.113.7:22041",
  "host": "203.0.113.7",
  "port": 22041,
  "user": "root",
  "hostKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI…"
}
```

- The sandbox has to be running. Asked again, the call changes nothing that is already so.
- `address` is where to connect now, with `host` and `port` beside it. The port leads to this sandbox's SSH server and to nothing else.
- It lets no key in. That is the next call.
- A machine that has no SSH server is given one the first time, and that request takes as long as the install does.

## Let a key in

`POST /v1/sandboxes/{id}/ssh/keys`

```shell
curl -X POST https://agental.io/v1/sandboxes/{id}/ssh/keys \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"publicKey":"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI… you@example.com"}'
```

- `publicKey` is one line, as it stands in your `~/.ssh/id_ed25519.pub`.
- The answer has the key's `id`, its `fingerprint` as `ssh-keygen -l` prints it, its `type` and its `comment`.
- A key that is already let in is not added twice.
- `GET /v1/sandboxes/{id}/ssh/keys` lists the keys, and `DELETE /v1/sandboxes/{id}/ssh/keys/{keyId}` takes one out. Whoever is logged in with it stays until they leave.
- The keys are the machine's own `/root/.ssh/authorized_keys`. A key written there from inside the sandbox is listed like any other.
- A fork has its source's keys, and a roll-back puts back the ones its snapshot had.

## Keys and SSH as a sandbox is made

A sandbox can be made with keys already let in, and with SSH already on, so that it can be logged in to as soon as the create answers.

```shell
curl -X POST https://agental.io/v1/sandboxes \
  -H "Authorization: Bearer $AGENTAL_API_KEY" \
  -H "Idempotency-Key: agent-1" \
  -H "Content-Type: application/json" \
  -d '{"name":"agent-1","sshKeys":["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI… you@example.com"]}'
```

- `sshKeys` on a create lets those keys in as the machine is made, before the answer. It turns nothing on.
- A template lets keys in to every sandbox made from it: `POST /v1/templates/{id}/ssh/keys`, with the same body as above, and `DELETE /v1/templates/{id}/ssh/keys/{keyId}` to take one out. A create's own keys come on top of its template's. While your account has [two-factor sign-in](https://agental.io/docs/two-factor) on, a key new to the template, let in from the console or sent by a person signed in to the site rather than with an API key, takes a `code` as well.
- `PUT /v1/templates/{id}/ssh` turns SSH on for each sandbox as it is made, and `DELETE` turns that off. `GET /v1/templates/{id}/ssh` says both, and the keys.
- A sandbox made already is not changed by its template's keys or SSH. A key taken out of a template stays in the sandboxes made from it until it is taken out of each.
- A create with keys takes the API key action `ssh`, as letting one in later does. So do the template's calls, apart from reading.
- Where a sandbox is made with SSH on and no port can be opened to it, the create still succeeds: SSH is on, and `PUT /v1/sandboxes/{id}/ssh` opens the port.

## Connect

Pin the host key the API gave you, then log in. With the answer above:

```shell
echo "sbx_2a9a47e50daec64a ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI…" > ~/.ssh/known_hosts_sbx_2a9a47e50daec64a

ssh -p 22041 \
  -o UserKnownHostsFile=~/.ssh/known_hosts_sbx_2a9a47e50daec64a \
  -o StrictHostKeyChecking=yes \
  -o HostKeyAlias=sbx_2a9a47e50daec64a \
  root@203.0.113.7
```

- `hostKey` comes over the API and not over SSH, so you never have to accept a key you are shown.
- Pin it under the sandbox's id, as above, and not under the address: an address and a port are another sandbox's in time.
- Only a key that was let in logs in. The SSH server takes no passwords.
- `scp`, `rsync` and `git` work at the same port.
- Only port 22 of the sandbox is reached this way. Its other ports are [public URLs](https://agental.io/docs/ports), or forwarded through SSH itself.

## After a stop or a move

SSH stays on for the sandbox. Its port is closed when the sandbox stops, and opened again by itself a moment after it runs again. To read where it is now:

`GET /v1/sandboxes/{id}/ssh`

- `address` is null while the sandbox is not running, and for a moment after a start.
- The address is mostly the same after a start. It is another one once the sandbox has been moved to another server, and connections that were open are lost at a move.
- So a client that loses its connection reads this call again, until it shows an address, and connects there.
- The host key stays the same through a stop and a start, and through a move.

## Turn it off

`DELETE /v1/sandboxes/{id}/ssh`

Closes the port, which cuts whoever is connected. The keys are left as they are.

## In the console

A sandbox's Network view has SSH under In, after its ports. It turns SSH on and off, shows the command to log in with and the host key, and lists the keys let in, each with a way to take it out, above a field that lets one in. The keys are read from the machine, so they show while the sandbox runs.

## Errors

| Answer | Meaning |
| --- | --- |
| `409 sandbox_not_running` | The sandbox is stopped. Start it, then repeat the request. |
| `409 ssh_unavailable` | The machine has no SSH server and one could not be installed or started, or no port could be opened to it. `message` says which. |
| `400 invalid_request` | `publicKey` is not one OpenSSH public key line. |
| `501 ssh_endpoint_not_configured` | Sandboxes here are not logged in to over SSH. `GET /v1/policy` says so as `ssh`. |

---

Every page of these docs as Markdown: [llms.txt](https://agental.io/llms.txt)
