Metadata-Version: 2.4
Name: torrus
Version: 0.2.59
Summary: Web-based SSH terminal that works behind any reverse proxy
Author-email: Anudeep Dhavaleswarapu <anudeepd2@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/anudeepd/torrus
Project-URL: Repository, https://github.com/anudeepd/torrus
Project-URL: Issues, https://github.com/anudeepd/torrus/issues
Keywords: ssh,terminal,web,xterm,remote
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Networking
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: fastapi>=0.110.0
Requires-Dist: python-socketio>=5.11.0
Requires-Dist: uvicorn[standard]>=0.29.0
Requires-Dist: paramiko>=3.4.0
Requires-Dist: aiofiles>=23.2.0
Requires-Dist: click>=8.1.0
Provides-Extra: ldap
Requires-Dist: ldapgate<0.2,>=0.1.29; extra == "ldap"
Provides-Extra: test
Requires-Dist: pytest>=8.0.0; extra == "test"
Requires-Dist: pytest-asyncio>=0.23.0; extra == "test"
Requires-Dist: pytest-mock>=3.12.0; extra == "test"
Requires-Dist: httpx>=0.27.0; extra == "test"
Requires-Dist: ruff>=0.8.0; extra == "test"
Dynamic: license-file

<p align="center">
  <img src="https://raw.githubusercontent.com/anudeepd/torrus/main/assets/logo.svg" alt="Torrus" width="120"/>
</p>

<h1 align="center">Torrus</h1>

<p align="center">A web-based SSH terminal that works behind any reverse proxy. Install it, run it, use it.</p>

## Features

- **Web-based SSH terminal** with full xterm.js emulation
- **SFTP file browser** — upload, download, rename, delete files directly from the browser
- **Multi-tab support** — open multiple SSH sessions side by side, close all at once
- **Tab management** — right-click to rename, clone, duplicate, or save a tab as a session
- **Keyboard shortcuts** — `Ctrl+,` / `⌘+,` settings; browser-reserved tab and omnibox shortcuts remain native
- **Saved servers** — save, edit, import, and export connection configs
- **Works behind reverse proxies** — uses Socket.IO for reliable transport
- **Session sidebar** — quick-connect to saved servers
- **Admin console** — LDAP-admin session inventory, owner-bound controls, and submitted-input activity view
- **LDAP/AD authentication** — optional, via [ldapgate](https://github.com/anudeepd/ldapgate)

## Install

```bash
pip install torrus
```

## Usage

```bash
torrus serve
```

Opens the terminal in your browser. Connect to any SSH server from there.
Torrus passes terminal input to the remote shell as entered. Bash's `TMOUT`
causes an interactive shell to exit after the configured idle time at its prompt.
If it is writable, run `unset TMOUT` in that shell. If Bash says it is readonly,
the setting is enforced by the shell startup configuration or host policy and
cannot be removed from that shell. Where host policy permits, a child shell may
be used by entering `bash` and then running `unset TMOUT` at the new prompt; this
only affects that child, and exiting it returns to the original shell with its
timeout. `bash && unset TMOUT` is different: it waits for the child to exit, then
runs `unset` in the original shell.

Options:

```
--host TEXT              Bind host. [default: 127.0.0.1]
--port INTEGER           Bind port. [default: 8080]
--no-browser             Don't open the browser automatically.
--ldap-config PATH       Path to ldapgate YAML config to enable LDAP authentication.
--ssl-keyfile PATH       TLS private key, to serve HTTPS directly.
--ssl-certfile PATH      TLS certificate matching --ssl-keyfile.
--log-file PATH          Write the server log to a file as well as stderr.
```

### Connecting to servers on your own network

Torrus refuses targets that are not publicly routable by default: private,
loopback, link-local, cloud-metadata and shared (CGNAT/Tailscale) addresses,
matched by name as well as by literal IP. This keeps the web UI from being used
as an SSH pivot into the network Torrus runs in — including into the host it
runs on, which is not exempt. A refused connection reports:

> Connections to private, loopback, link-local or shared (CGNAT) addresses are not allowed. Set TORRUS_ALLOW_PRIVATE_HOSTS=true to allow them.

Set the variable when the machines you connect to are on your LAN:

```bash
TORRUS_ALLOW_PRIVATE_HOSTS=true torrus serve
```

### Environment variables

| Variable | Default | Purpose |
|---|---|---|
| `TORRUS_ADMIN_USERS` | *(empty)* | Comma-separated LDAP usernames allowed to open the Admin Console. |
| `TORRUS_AUDIT_DB` | `~/.local/share/torrus/audit.db` | Audit database location (`XDG_DATA_HOME` is honoured). |
| `TORRUS_LDAP_CONFIG` | *(empty)* | Same as `--ldap-config`, for deployments that set it in the service unit. |
| `TORRUS_LOG_FILE` | *(empty)* | Same as `--log-file`. |
| `TORRUS_DEV` | *(empty)* | Development mode: relaxed socket origins for the Vite dev server. |
| **`TORRUS_SSH_HOST_KEY_POLICY`** | `accept-new` | **`accept-new` records an unknown host key and refuses one that changed; `strict` also refuses an unknown host.** |
| `TORRUS_SSH_KNOWN_HOSTS` | `~/.local/state/torrus/known_hosts` | Where host keys trusted on first use are written (`XDG_STATE_HOME` is honoured). The system `~/.ssh/known_hosts` is always read as well. |
| **`TORRUS_ALLOW_PRIVATE_HOSTS`** | `false` | **Allow connecting to private, loopback, link-local, cloud-metadata or shared (CGNAT/Tailscale) addresses, including by name. Off by default: without it, torrus cannot be used as an SSH pivot into the network it runs in.** |
| `TORRUS_ALLOW_PRIVATE_HOSTS_WITHOUT_LDAP` | `false` | Deprecated alias for the above; kept so an existing deployment does not change behaviour silently. |
| `TORRUS_MAX_SSH_SESSIONS` | `128` | Ceiling on live SSH sessions. Each one occupies a worker of the shared I/O pool, so the default matches it: the alternative is every terminal stalling at once. |
| `TORRUS_BULK_ZIP_MAX_BYTES` | `2147483648` | Largest "download as zip" selection. Larger trees are refused before the archive starts streaming. |
| `TORRUS_MAX_UPLOAD_BYTES` | `1099511627776` | Largest single upload. |
| `TORRUS_UPLOAD_CHUNK_BYTES` | `33554432` | Upload window size. With LDAP auth it is capped at ldapgate's `proxy.max_body_size` (default 10 MB), which otherwise rejects larger windows with a 413. |
| `TORRUS_UPLOAD_CONCURRENCY` | `4` | Upload windows in flight per file. |
| `TORRUS_UPLOAD_SESSION_TTL` | `3600` | Seconds an interrupted upload stays resumable. |
| `TORRUS_SFTP_INLINE_MAX_BYTES` | `5242880` | Largest file served inline rather than as a streaming download. |

Host keys: Torrus records a host's key the first time it connects (trust on
first use) and **refuses the connection if that key later changes**, printing
both fingerprints. A host whose key legitimately changed needs its entry removed
from the store above. Set `TORRUS_SSH_HOST_KEY_POLICY=strict` to also refuse
hosts it has never seen.

## LDAP Authentication

Torrus can require users to log in via LDAP/AD before accessing the terminal. This uses [ldapgate](https://github.com/anudeepd/ldapgate) as FastAPI middleware — no separate proxy process needed.

```bash
pip install 'torrus[ldap]'
torrus serve --ldap-config /path/to/ldapgate.yaml
```

When LDAP is enabled, a logout button and (for configured admins) an admin console button appear in the top-right corner of the tab bar.
LDAPGate logout and browser/socket disconnects do not immediately terminate
remote SSH sessions. Sessions attached to a live browser socket remain available
while idle; detached sessions are reaped after four hours to allow time to reconnect.
Explicitly close a terminal tab to end its remote session. For plain HTTP
deployments with LDAPGate, configure `proxy.secure_cookies: false`; secure-cookie
mode requires HTTPS.

Set `TORRUS_ADMIN_USERS` to a comma-separated list of LDAP usernames allowed to use the console:

```bash
TORRUS_ADMIN_USERS=alice,bob torrus serve --ldap-config /path/to/ldapgate.yaml
```
Session controls use immutable session identity plus generation checks. New user allowlist entries and disable/re-enable changes apply immediately without restarting Torrus; disabling a user revokes known LDAP cookies and closes that user's active SSH tabs. Policy mutation requires LDAPGate user-wide revocation support (0.1.22+); older deployments fail closed without changing policy.

### Uploads behind LDAPGate

Uploads arrive at the gate in `TORRUS_UPLOAD_CHUNK_BYTES` windows, and ldapgate
answers any request body over `proxy.max_body_size` with a bare 413 before the
request reaches Torrus. Its default is 10 MB, so keep the setting at or above the
window:

```yaml
proxy:
    max_body_size: 33554432   # 32 MiB, matching TORRUS_UPLOAD_CHUNK_BYTES
```

Torrus caps the window at that limit on startup and logs a warning naming the
setting to raise. If another reverse proxy rejects a chunk with HTTP 413, the
browser halves the request and retries, down to 64 KiB. Larger body limits still
improve throughput by reducing the number of requests. Raise both together, or
lower both to 16 MiB if you would rather retry smaller windows on a flaky link.

### Terminal input audit

LDAP deployments persist completed command lines after Enter, not raw
keystrokes or terminal output. Pasted multiline input is split into submitted
lines; the Admin Console's **Submitted input** table groups consecutive command
lines from the same session into one block (view only — stored events stay one
row per line), preserves embedded line breaks, wraps long values, and lets an
admin expand truncated previews.
Inputs entered after a detected password/passphrase/token prompt, plus command
lines containing inline credential flags or assignments, are stored only as a
`[redacted sensitive input]` marker. The password supplied while opening an
SSH connection is never recorded. Audit data is stored at
`~/.local/share/torrus/audit.db` by default (or `TORRUS_AUDIT_DB` when set).

```bash
torrus audit show --user alice
torrus audit purge --older-than 90
```

`audit show` escapes control characters so viewing an event cannot replay its
terminal escape sequences. Command text can still contain secrets that do not
match the sensitive-input detector; restrict access to the audit database.

The Admin Console's **Submitted input** view displays completed command events
to authorized admins; sensitive events show only their redaction marker.

### SFTP audit
LDAP deployments also record SFTP file operations: every download (inline,
streaming, or a bulk zip archive — one event per archived file) plus uploads,
deletes, renames, mkdirs, chmods, and chowns. Each event stores the resolved
remote path, the size for transfers, and op-specific detail (rename target,
permission mode, owner/group ids). Downloads are recorded when initiated;
uploads when completed. Events appear in `torrus audit show` and in the
Admin Console's activity table (kind `sftp_<operation>`), and they are
covered by the same retention purge as terminal input.


## Development

Requires [uv](https://github.com/astral-sh/uv).

```bash
git clone https://github.com/anudeepd/torrus
cd torrus
uv sync
make dev
```

See [RELEASING.md](RELEASING.md) for the CI and release workflow.

## License

MIT
