Skip to main content

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:
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:
  2. Authorize your public key. The file uses OpenSSH’s authorized_keys format:
  3. Restart the daemon (mirage daemon restart) and create a workspace with an id you can type:
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:

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. 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 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: