---
id: "omnitron/terminal"
title: "SSH workspace"
description: "Multiple SSH tabs through Xec, with pinned host keys, live authorization and bounded sessions shared with CLI."
---

# SSH workspace

Open **SSH workspace** from the sidebar, or **SSH** on a registered node. Each tab creates an independent interactive shell using the SSH identity configured for that node. The workspace supports split view, terminal search, font controls, copy selection, a local read-only mode and review before pasting line breaks or control characters.

The service runs on a master with PostgreSQL-backed authentication and audit. The daemon owns the SSH connection. Credentials stay on the server. The browser renders the terminal through lazily loaded xterm.js; the CLI uses the same `OmnitronTerminal` RPC service. SSH runs through Xec's existing `NodeSSH` implementation, exposed by `@xec-sh/core/ssh`.

## Integration boundary

The terminal service uses Xec's shell, host-key and connection-lifecycle APIs.
It must not construct a separate SSH client or duplicate authentication and
connection policies. The Xec source candidate makes its connection reference a
lifecycle-event facade; raw transport execution stays private. SSH channel and
SFTP types are exported by Xec itself. Existing terminal event subscriptions fit
this boundary; new capabilities belong in Xec before Console or another application
uses them. See the [release checklist](/docs/omnitron/release-readiness) for activation status.

## Working with tabs

The breadcrumb trail identifies the workspace without a duplicate page title. The node selector and **New SSH tab** stay visible; **Workspace actions** contains host-key verification, the equivalent CLI command and the audit trail.

The layout control reads **1 pane** by default: only the selected tab is shown. Selecting **2 panes** explicitly enables split view, showing the selected tab and another open session. Each pane is labeled with its stable tab name. The workspace shares the available space between the two sessions: side by side when the content area is at least 960 px wide, stacked otherwise. Both panes stay inside the window. Closing down to one tab resets the layout to one pane. Scrollback scrolls within each terminal, and resizing the window or sidebar updates the remote PTY dimensions. Tabs remain connected when following Console navigation, including breadcrumbs.

Each pane exposes input mode, search and a **Terminal actions** menu. **READ** disables local keyboard and paste input; it does not change the remote account's permissions. Search opens with the magnifying glass or **Ctrl/⌘ + Shift + F**. Use **Enter** for the next match, **Shift + Enter** for the previous match and **Escape** to return to the shell. The actions menu provides copy selection and text sizes from 10 to 24 px, with a 14 px reset. Every tab has its own close button. **Delete** on a focused tab requests the same action. A running session requires confirmation naming its target, including when its tab is inactive. A session already reported as closed is removed locally without another confirmation or RPC call. If the shell exits while confirmation is open, the pending action becomes local tab cleanup. Closing the active tab selects its neighbor; closing the last tab returns focus to **New SSH tab**. Tab numbers remain stable when another tab is removed.

## Connection and closure feedback

**New SSH tab** creates a pending tab immediately. It names the node and SSH
identity, shows elapsed waiting time and stays responsive while the daemon opens
the shell. There is no invented percentage or handshake stage. Switching to
another tab keeps that selection when the new connection completes. Duplicate
open requests are blocked until the attempt returns.

A rejected connection displays the reason in its tab and offers **Retry
connection**. A transport deadline instead reads **unconfirmed**: the remote
operation may still have happened. Unattached shells are subject to the daemon's
client-disconnection timeout; a failed browser request is not proof of closure.
Failed node/policy discovery has a separate retry and does not leave a permanent
loading state.

Closed panes retain their scrollback, exit reason and exit code when available.
After a read/write failure, input stops immediately and Console requests closure.
The session is shown as **State unknown** until that closure is acknowledged. A
failed manual close keeps the tab and the confirmation dialog, with a visible
reason and retry. Successful closure removes the tab. Read-only input mode does
not override a closed session or an unavailable connection.

## Verify the host before connecting

1. Register a node with its SSH hostname, port, user and credential using the [node commands](/docs/omnitron/cli#nodes-infrastructure-machines).

2. Open **Workspace actions** (the ellipsis beside the node selector), then select **Verify host key**. The daemon observes the server's key in a handshake that does not authenticate.

3. Compare the SHA256 fingerprint with an independently trusted source, such as the host's provisioning console. For an OpenSSH host, an administrator can run:

   ```bash
   ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub -E sha256
   ```

4. Enter the verified fingerprint to save trust, then select **New SSH tab**.

An observed fingerprint is not proof of host identity. Verification must not consist of copying a key from the same untrusted network connection. Pin replacement uses the previous pin as a compare-and-swap condition and closes sessions for that node. A pin applies to the registered host, port and username; changing that target requires verification again.

The same registered fingerprint protects automated commands, file uploads and mesh tunnelling. For an unpinned automation target, Xec can use an existing trusted `known_hosts` entry on the controller. Interactive sessions require a verified registration pin. A stack that selects a different SSH account or port must provide its own credentials.

When replacing a registered host, SSH port or account, provide or explicitly clear its credentials in the same update. If saving the new credentials fails, the replacement registration remains without active credentials. Retry the update on that registration and verify its host fingerprint before use. An already dispatched remote command can still finish after a trust change.

Pins are stored in the daemon's SQLite state store. Opening a terminal requires an exact pin match before authentication. Host-key probing and terminal opening use Xec's bounded connection and shell-request lifecycle. SSH adapter pools keep distinct host-key policies separate and preserve them during reconnect.

## Equivalent CLI workflow

```bash
omnitron node ssh-key edge-1
omnitron node ssh-key edge-1 --trust SHA256:<independently-verified-fingerprint>
omnitron node ssh edge-1
omnitron terminal list
omnitron terminal close <session-id>
```

The node selector accepts a registered name or ID. `Ctrl+]` closes the CLI terminal and restores local terminal settings. **Copy CLI command** in Console produces the same node command.

Terminal listing and closing are limited to the caller's owner identity. Local CLI sessions belong to the OS-authorized local daemon identity; browser sessions belong to the human account and its specific authenticated session. Another browser sign-in, even for the same account, cannot read or control an existing terminal.

## Session lifetime and limits

| Setting                            | Default            |
| ---------------------------------- | ------------------ |
| Enabled                            | `true` on a master |
| Active sessions across the daemon  | 32                 |
| Active sessions per owner          | 8                  |
| Idle without input                 | 15 minutes         |
| Absolute lifetime                  | 8 hours            |
| No output reads from the client    | 90 seconds         |
| Retained server output per session | 256 KiB            |
| Maximum output read                | 64 KiB             |
| Maximum input per RPC              | 8 KiB              |
| Closed-session retention           | 60 seconds         |

Configure policy in the daemon configuration:

```typescript
export default {
  daemon: {
    terminal: {
      enabled: true,
      maxSessions: 32,
      maxSessionsPerOwner: 8,
      idleTimeoutMs: 15 * 60_000,
      absoluteTimeoutMs: 8 * 60 * 60_000,
      disconnectTimeoutMs: 90_000,
    },
  },
};
```

Counts must be positive integers; the supported upper bounds are 128 daemon-wide and 32 per owner. `enabled: false` disables terminal creation. Build and restart the daemon to apply new backend code or startup configuration; a frontend rebuild alone does not register new RPC services.

Output travels as base64 bytes with an absolute byte cursor, preserving UTF-8 boundaries and terminal protocols. A slow reader receives an explicit dropped-byte count. `hasMore` allows the client to drain retained output after a shell closes. Input is sequenced and deduplicated; an ambiguous write failure closes the session instead of replaying a command.

Closing a session is idempotent after its retained server state expires, so an
old tab can still be removed. This operation still requires current administrator
authority. A live session owned by another sign-in remains inaccessible.

## Authorization and audit

SSH requires the administrator role and a current human session, or the trusted local Unix-socket identity. A machine `service_role` cannot open an interactive terminal. The daemon rechecks authorization and ownership on every method; its two-second sweep also closes revoked sessions, removed targets and expired shells.

Host trust and terminal opening require an audit write before proceeding. The audit records target identity, ownership and lifecycle events. It deliberately does not record commands, terminal output, passwords or key material. Use **Audit** or `omnitron audit --resource terminal`. An audit of session lifetime is not a shell transcript.

Use HTTPS outside loopback. xterm.js runs without automatic URL opening or OSC 52 clipboard writes. Multi-line/control-character paste requires review. **Read only** disables input for that pane; it is a local usability control, not a separate server permission. The remote shell still has the full OS rights of the registered SSH user.

## What persists

Tabs and their output survive navigation inside the current Console. They do not survive a browser reload or daemon restart. Closing the browser ends the session after the disconnect timeout. No commands are automatically replayed and no shell is automatically reattached. Use a remote terminal multiplexer when the workload itself must outlive a connection.

This workspace provides interactive PTY shells. File transfer, jump-host configuration, SSH agent forwarding, shared sessions and durable session recording are not exposed here. They require separate permission and lifecycle design before being added.

See [Console access](/docs/omnitron/access), [Xterm security guidance](https://xtermjs.org/docs/guides/security/) and [OpenSSH fingerprint inspection](https://man.openbsd.org/ssh-keygen.1).