> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirage.strukto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# SSH Access

> Reach a workspace with ssh, sftp and scp through the daemon's SSH door.

## What It Does

The daemon can listen for SSH next to its HTTP API. Any SSH client then reaches a workspace the way it reaches a machine: `ssh` opens a shell or runs a command, and `sftp` and `scp` move files. Nothing is installed anywhere and there is no real host behind the door; every command runs in mirage's shell and every file operation goes through the workspace's mounts.

The SSH username names the workspace:

```bash theme={null}
ssh -p 2222 demo@127.0.0.1                  # interactive shell in workspace "demo"
ssh -p 2222 demo@127.0.0.1 'ls /data | wc -l'   # one command
sftp -P 2222 demo@127.0.0.1                 # file transfer
scp -P 2222 notes.txt demo@127.0.0.1:/data/  # copy in
```

The door is off until a port is set, so a daemon nobody configured for SSH never opens a second port.

## Turn It On

1. Pick a port and add it to `~/.mirage/config.toml`:

   ```toml theme={null}
   [daemon]
   ssh_port = 2222
   ```

2. Authorize your public key. The file uses OpenSSH's `authorized_keys` format:

   ```bash theme={null}
   mkdir -p ~/.mirage/ssh
   cat ~/.ssh/id_ed25519.pub >> ~/.mirage/ssh/authorized_keys
   ```

3. Restart the daemon (`mirage daemon restart`) and create a workspace with an id you can type:

   ```bash theme={null}
   mirage workspace create workspace.yaml --id demo
   ssh -p 2222 demo@127.0.0.1
   ```

The file is read again on every login, so adding or revoking a key takes effect without a restart. On first start the daemon mints an ed25519 host key at `~/.mirage/ssh/host_ed25519_key` and keeps it, so your `known_hosts` entry stays valid across restarts. Both daemons read and write the same OpenSSH key format, so either can serve the other's key.

An `~/.ssh/config` entry saves the flags:

```text theme={null}
Host mirage-demo
  HostName 127.0.0.1
  Port 2222
  User demo
```

## Settings

Per key the environment variable wins over the `[daemon]` table, which wins over the default. `mirage config list --resolved` shows each one's effective value and where it came from, and `mirage config set ssh_port 2222` writes it.

| Key                   | Env var                      | Default                          | Meaning                                              |
| --------------------- | ---------------------------- | -------------------------------- | ---------------------------------------------------- |
| `ssh_port`            | `MIRAGE_SSH_PORT`            | unset (off)                      | Port to listen on.                                   |
| `ssh_host`            | `MIRAGE_SSH_HOST`            | `127.0.0.1`                      | Interface to bind. Loopback only, like the HTTP API. |
| `ssh_host_key_file`   | `MIRAGE_SSH_HOST_KEY_FILE`   | `~/.mirage/ssh/host_ed25519_key` | The server's private host key.                       |
| `ssh_authorized_keys` | `MIRAGE_SSH_AUTHORIZED_KEYS` | `~/.mirage/ssh/authorized_keys`  | Public keys allowed to log in.                       |

The Python daemon needs the `ssh` extra (`pip install 'mirage-ai[ssh]'`); the TypeScript daemon needs `ssh2` installed beside it (`npm install ssh2`). A configured door that cannot open (the port is taken, the library is missing) fails the daemon's start instead of leaving it up without SSH.

## What a Login Gets

* **One session per channel.** Every `ssh` command and every shell runs as a fresh mirage session under the workspace's default profile, just as each channel is a fresh process under sshd. A `cd` or `export` never leaks from one channel to another, and two channels never wait on each other's lines. The session is closed when the channel ends.
* **Profiles and policies apply.** SSH lines go through `Workspace.shell`, and SFTP goes through the same mount core the [FUSE mount](/home/setup/fuse) uses, so mount modes, hides, asks and policies answer exactly as they do for a shell in that session. A read-only mount refuses an `sftp put` with "Permission denied".
* **A login environment.** `HOME` is `/`, where every session starts, `USER` and `LOGNAME` are the workspace id, `SSH_CLIENT` and `SSH_CONNECTION` describe the connection, and `TERM` comes from the client's terminal. A variable the profile already set keeps its value, and each one clears the session's `pre_session` gate like any `export`.
* **History.** Typed lines land in `/.bash_history` like any other top-level line. The login environment line does not.
* **An interactive shell.** With a terminal (`ssh` with no command), the door prints `mirage:<cwd>$ `, echoes and edits the line, and treats Ctrl-C as an interrupt: the running line is cancelled and `$?` reads 130, as in bash. Ctrl-D on an empty line or `exit [N]` ends the shell. Without a terminal (`ssh -T host < script`), lines are read from stdin with no prompt, the way `bash -s` reads them.
* **Bounded input.** A non-terminal shell line may contain up to 1 MiB before its newline. Longer lines close the channel with status 1 and `mirage: shell input line too long`; no prefix is executed. Streaming stdin to a command is not subject to the line limit. Terminal editors ring the bell and ignore additional input at their line limit; Backspace and Ctrl-U still work.
* **The client's exit status.** `ssh host cmd` exits with the line's status, and a dropped connection cancels the line it was running.

Only public keys are accepted: no passwords, no keyboard-interactive. Nothing is forwarded: no ports, no agent, no X11. A subsystem other than SFTP is refused.

## Differences Between the Daemons

The two daemons serve the same door, with these differences:

|                                                      | Python                                                      | TypeScript                                                                                                        |
| ---------------------------------------------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| Legacy `scp -O` (the SCP protocol)                   | Served.                                                     | Not served. Modern `scp` uses SFTP and works on both.                                                             |
| Terminal line limit                                  | 1024 characters.                                            | 1024 bytes.                                                                                                       |
| Command output                                       | Streamed as the line produces it.                           | Sent when the line finishes, as the HTTP API sends it.                                                            |
| `authorized_keys` options (`from=`, `command=`, ...) | Enforced by asyncssh; `command=` forces the line that runs. | Not interpreted, so a key line carrying options is skipped with a warning rather than accepted as if it had none. |
