An open-source SSH workspace with a black-and-white, Linear/Vercel-grade UI. Manage many servers, monitor GPUs live, run an interactive terminal in the browser, and operate Docker / tmux / systemd / processes without dropping to the CLI. Real multi-user accounts, per-server access control, and an audit log — self-hostable behind your own identity provider or standalone.
Black & white theme · React + Tailwind (shadcn-style) frontend · Python stdlib backend. Everything runs on infrastructure you control. No AI features. No telemetry.
One command — starts the backend and the web app:
cd web
npm install # first time only
npm run dev # boots the Python backend (if not already up) + the web UIThen open http://localhost:5180. (start.bat still works if you prefer a double-click.)
On first run, a local admin account is created automatically and its generated password is
printed once to the console — log in with it, then change it from Admin → Users.
You connect to each host from the UI itself — pick a server and hit Connect (one click). Disconnect terminates the session.
Optional — full in-browser terminal (htop/vim/tmux):
pip install pywinptyWithout it the in-browser terminal still works (interactive shell, colors) via a piped fallback, and Native terminal always opens a full TTY in Windows Terminal.
| Module | What it does |
|---|---|
| Servers | Multi-server sidebar with folders, live status, one-click add/edit. Connect by key, SSH config alias, Telnet (username/password), or local. Upload a private key straight from the browser — it lands on whichever machine runs the backend, not just wherever you're typing from. Optional Wake-on-LAN (magic packet before connecting), outbound SOCKS5 proxy, and TCP port-knocking (SSH hosts) — per-host or as an admin-wide default. |
| Connect / Disconnect | Start and terminate a persistent SSH or Telnet session per host from the UI. Sessions survive terminal-tab switches and browser reloads; Disconnect kills them. |
| Overview | Live GPU cards (util/mem/temp/power/fan) with sparklines + history chart, system metrics (CPU/RAM/load/disk), and independent TCP reachability polling (up/down even if SSH/Telnet itself is what's hung). |
| Alerts | Three tabs — Alerts (firing/resolved feed), Rules (threshold conditions), Channels (delivery: ntfy, webhook, or email via admin-configured SMTP). |
| Terminal | Real interactive SSH or Telnet terminal in the browser (xterm.js + WebSocket PTY/socket bridge). Tabs + split panes, persistent sessions, reconnect, optional session recording. A Telnet host has its own port (default 23), optional username/auto-login password, and optional auto-login delay — everything else (Console, file browser, Docker/tmux/services/network, SSH key manager, tunnels) stays SSH-only, since Telnet has no per-command exec channel to run them over. |
| Job Queue | GPU compute processes (pid/user/mem/elapsed) with Kill. |
| Console + Run on all | Command runner per host (OS-aware presets), plus cross-server execution across selected connected servers. |
| Docker | Containers, images, volumes, networks — start/stop/restart/logs/remove + system prune. |
| tmux | Sessions list with Kill (guards shared sessions). |
| Services | Running systemd units with Restart. |
| Network | Interfaces + listening ports. |
| Logs | Live tail -f of a file / journal streamed over WebSocket. |
| Notifications | Bell + desktop notifications driven by GPU temp/memory/reachability threshold alerts. |
| Tunnels | SSH port forwarding — saved Local / Remote / SOCKS tunnels with start/stop + live status. |
| Keys | SSH key manager — list (with fingerprints), generate, copy public key, authorize a key, delete, or upload an existing one. |
| Files | Remote browser with upload / download / rename / delete and a Monaco editor (save). |
| Quick Connect | One-off connection (host/port/user + password, key, or a saved credential) without adding it as a saved server — connect to a terminal or the file browser directly, with an optional "save as server" follow-up. |
| Jump hosts | Route a server's SSH connection through one or more intermediate hops, configured as an ordered chain on the host editor (ssh -J under the hood — no protocol work, just argv). |
| Credentials | Reusable stored secrets (password or key), decoupled from any single host, so one credential attaches to many servers instead of re-entering it per host. |
| Snippets | Saved-command library in folders, searchable, insertable straight into the focused terminal tab, with JSON export/import. |
| Session Logs | Every terminal session's full text transcript, browsable by host/start-time/duration, with a read-only playback viewer — off entirely via the global Command History admin toggle. |
| Connections | Live read-only list of every currently-open session (terminal, files, …) across the app, for at-a-glance "what's active right now." |
| Personal ID | Claim a handle and publish your SSH public key(s) at a stable, unauthenticated URL (/pubkeys/<handle>) — pipe it straight into authorized_keys on any box instead of copy-pasting. |
| Backup / Restore | Admin-only full-instance export (auth.db + every JSON config store, zipped) and restore, using SQLite's own online backup API — safe even with the app live. |
| Host quick actions + customizable metrics dashboard | Per-host one-click command buttons in the Overview toolbar, plus drag-to-reposition/resize widget cards (GPU, history, system stats, reachability) — per user, per host, with a reset-to-default. |
| Admin | Users (create/delete, per-server access grants), Auth & Session policy (local/OIDC/2FA toggles, session timeout, self-service password reset, silent SSO), SSO provider CRUD, active sessions (device/OS, revoke), API keys for programmatic access, host defaults (global new-host toggles), and a durable audit log. Admin-only. |
| Command palette | ⌘K / Ctrl+K — jump to any server, module, or action. |
All modules are OS-aware: remote Linux hosts use shell tooling; local "This PC" uses PowerShell equivalents (Get-Service, Get-NetTCPConnection, ssh-keygen, etc.) — Windows-local only for now (see Roadmap).
Real accounts, not "anyone who reaches the port has full control." Two modes, pick one via
the AUTH_MODE environment variable:
AUTH_MODE=local (default) — the app's own login. Usernames/passwords, hashed with
PBKDF2-HMAC-SHA256 (stdlib hashlib, no external crypto dependency), stored in a local
SQLite file (auth.db, created automatically, 0600 permissions). First run bootstraps one
admin account (see ADMIN_USERNAME/ADMIN_PASSWORD below). Good default for a standalone
install with no other identity system in front.
AUTH_MODE=trusted_header — for deployments already sitting behind an external
forward-auth reverse proxy (Caddy/nginx/Traefik + oauth2-proxy or similar, backed by any
OIDC provider — Pocket-ID, Authelia, Keycloak, Authentik, Google Workspace, etc.). The app
trusts the X-Forwarded-Email header the proxy sets after it verifies identity, and
auto-provisions/recognizes a user per distinct email — no separate local login screen at
all. Requires REQUIRE_FORWARDED_EMAIL=1 too (refuses to start otherwise) — without
that, the header would be unverified client-supplied input, not a real identity signal.
Either mode gets the same RBAC (below), audit log, and session/identity model — the only difference is where the credential actually lives.
Optional external sign-in tab (works alongside AUTH_MODE=local): the login screen can
show two tabs at once — Login (username/password) and External (one or more OIDC
providers) — so a subset of users can sign in with an outside identity provider without
switching the whole deployment to trusted_header. This app drives the redirect and code
exchange itself (Authorization Code + PKCE, stdlib urllib.request/hashlib/secrets only,
no extra dependency), then auto-provisions a local account per distinct email the provider
returns — same identity model as trusted_header, just triggered by a button instead of a
proxy header. Two ways to add a provider, usable together:
- Env vars (
OIDC_*below) — one provider, set at deploy time, endpoints found via the issuer's/.well-known/openid-configuration. Leave any of the five unset and it simply doesn't show up. - Admin → Auth & Session — any number of providers, added/edited/removed at runtime,
each with its client ID/secret and the three endpoint URLs entered directly (no
discovery). The same panel has a toggle for the Login tab itself, in case a
deployment wants to go external-only without redeploying with
AUTH_MODE=trusted_header. The server refuses to leave zero sign-in options enabled, so you can't lock yourself out through this panel.
Every provider — env-configured or admin-added — redirects back to the one
OIDC_REDIRECT_URI below, since they all return to this same app instance.
Two-factor auth (TOTP): optional per-account second factor (RFC 6238, stdlib
hmac/hashlib/base64 only — no authenticator-app SDK needed), with a "remember this
device for 30 days" option that skips the code prompt on a browser that already proved it
once. Login attempts and second-factor checks are both rate-limited (per account and per
source address) to make online guessing impractical.
Self-service password reset: a "forgot password" flow where the one-time code is logged server-side (console/journal) rather than emailed — no mail infrastructure required. Toggled from Admin → Auth & Session; off by default.
Active sessions & API keys: Admin → Sessions lists every live login (device/OS label, issued/expiry, a "YOU" badge) with one-click revoke. Admin → API Keys issues bearer tokens for programmatic access to this app's own HTTP API, independent of a browser session.
RBAC: every server entry can be scoped to specific non-admin users via Admin → Users → (expand a user) → toggle per-server access. Admins always see/use everything; server management itself (create/edit/delete a server, upload a key) is admin-only.
Custom roles: a second, narrower layer on top of the per-server access above. Admin →
Users → Roles creates named capability sets (view_hosts, exec_commands, manage_tunnels)
-- e.g. a "read-only operator" role with only view_hosts can see a host's status, metrics,
and logs but can't open a terminal, run a command, edit a file, or touch a tunnel on it. Each
non-admin user gets a role picker on their row in Admin → Users; leaving it unset ("No role")
keeps that account's original full access within whatever servers it's already granted --
assigning a role only ever takes capabilities away, never grants anything beyond that.
Audit log: every login, connect/disconnect, command execution, kill/restart action, and user/access change is recorded with who, what, where, when, and success/failure. Admin-only viewer under Admin → Audit log.
| Variable | Default | Meaning |
|---|---|---|
PORT |
8765 |
backend port |
AUTH_MODE |
local |
local or trusted_header |
ADMIN_USERNAME / ADMIN_PASSWORD |
(generated) | seed the first admin account explicitly (local mode only); unset after first boot |
REQUIRE_FORWARDED_EMAIL |
unset | when set, every request must carry a non-empty X-Forwarded-Email header — required for trusted_header mode, optional extra layer in local mode if you also sit behind a proxy |
EXTERNAL_LOGOUT_URL |
unset | where the frontend's Sign Out button sends the browser in trusted_header mode, to actually end the session at your identity provider (e.g. https://auth.example.com/oauth2/sign_out?rd=https://ops.example.com/) |
OIDC_PROVIDER_NAME |
unset | display name for the legacy env-configured provider's button (e.g. Authentik); all five OIDC_* variables must be set for it to appear |
OIDC_ISSUER |
unset | that provider's issuer URL — {issuer}/.well-known/openid-configuration must resolve |
OIDC_CLIENT_ID / OIDC_CLIENT_SECRET |
unset | credentials for the OIDC client registered with that provider |
OIDC_REDIRECT_URI |
unset | this app's own callback URL, shared by every provider (env-configured or admin-added) — must exactly match what's registered with each one, e.g. https://ops.example.com/api/auth/oidc/callback |
OIDC_SCOPES |
openid email profile |
scopes requested by the legacy env-configured provider (admin-added providers set their own scopes in Admin → Auth & Session) |
The shape that works with any OIDC-capable reverse-auth proxy (this project doesn't bundle one — bring your own: oauth2-proxy is a solid, free choice):
your reverse proxy (Caddy/nginx/Traefik)
-> forward_auth to oauth2-proxy (verifies session, talks OIDC to your provider)
-> on success, sets X-Forwarded-Email (or equivalent) and proxies to this app
With Caddy + oauth2-proxy specifically, the relevant site block looks like:
your-ops-domain.example.com {
forward_auth 127.0.0.1:4180 {
uri /oauth2/auth
copy_headers X-Auth-Request-Email
@error status 401
handle_response @error {
redir * https://your-auth-domain.example.com/oauth2/sign_in?rd={scheme}://{host}{uri}
}
}
reverse_proxy 127.0.0.1:8765 {
header_up X-Forwarded-Email {http.request.header.X-Auth-Request-Email}
}
}Two easy-to-miss oauth2-proxy settings this depends on: set_xauthrequest = true (without
it, /oauth2/auth never returns the email header for copy_headers to copy — every request
silently arrives with an empty identity), and an exact-match allowlist via
authenticated_emails_file if you want to restrict to specific people (email_domains
matches a domain, not a full address — setting it to a full email address matches nothing,
ever).
ops-dashboard/
server.py Python stdlib backend: REST API + WebSocket terminal bridge
auth.py users/sessions/RBAC/audit log (SQLite, stdlib sqlite3)
auth.db created on first run, gitignored, 0600
servers.json saved servers (edit from the UI), gitignored
keys/ uploaded private keys, gitignored, 0700 dir / 0600 files
static/ production frontend build output (or a placeholder page in dev)
web/ React + Vite + Tailwind frontend (the app)
The web app proxies /api and /ws to the Python backend, which reaches each host over
your SSH config / key. Every request (except the static shell and the auth endpoints
themselves) requires a valid session; mutating actions are further validated server-side
(RBAC scoping, path/input sanitization) regardless of what the UI allows.
Docker Compose packaging · GPU job scheduler · RDP/VNC connection types (both need a real protocol-gateway service — a proprietary binary protocol and, for VNC, a block cipher for the auth handshake — neither fits in Python stdlib; Telnet, needing neither, already shipped below) · more themes (OLED, Nord, Catppuccin) · Linux/macOS parity for "This PC" local mode (currently Windows-only) · Kubernetes.
Note on the Credentials manager: stored secrets live in the same auth.db SQLite file as
password hashes, protected the same way (filesystem 0600, never returned to the frontend) —
not encrypted at rest with a separate key, since stdlib has no block-cipher primitive and a
hand-rolled one isn't worth shipping. Fine for a self-hosted single file already protected by
OS permissions; know that before storing something you wouldn't want readable by whoever can
read auth.db.
_Shipped: multi-user accounts (local password, trusted-header/OIDC-proxy, or a native external-provider tab with admin-managed multi-provider config) · TOTP 2FA with device trust · self-service password reset · active-session & API-key management · rate-limited login · RBAC · durable audit log · multi-server connect/disconnect · Quick Connect · jump-host chaining · credentials manager · live GPU + history + alerts (rules/channels, incl. email) & desktop notifications · independent TCP reachability monitoring · terminal tabs & split · session recording + log browser · per-terminal command history · SFTP file ops + Monaco editor · port forwarding · outbound SOCKS5 proxy + TCP port-knocking · SSH key manager incl. browser upload · Personal ID public-key publishing · snippets manager · full backup/restore · host quick actions
- customizable metrics dashboard · connections tracker · Docker (containers/images/volumes/ networks) · tmux · services · network · live log tail · cross-server execution · command palette · Wake-on-LAN before connecting · Telnet connection type (own port/auth/auto-login, terminal, and session recording, alongside SSH)._
Several hosts may be shared with other people. The UI warns before killing tmux sessions/processes. Confirm dialogs guard destructive actions. Server management and key upload are admin-only; non-admin access is explicitly scoped per server.