Skip to content
sm26449Public

About

Self-hosted web terminal & SSH bastion for your servers: persistent tmux sessions from any browser or phone. Outbound-only signed agent (no open ports), passkeys/SSO, files, port forwarding, Docker, audit log.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Latest commit

 

History

526 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WebTerm

CI License: MIT Version

Persistent terminals for your whole infrastructure, in the browser.

Open a shell on any of your machines from a browser — including a phone — and come back hours later with the process still running and the scrollback intact: the session lives in tmux on the host, not in the gateway. A small agent dials out to the gateway, so nothing listens on your servers and a machine behind NAT works like one with a public IP; SSH and telnet cover the gear you cannot install anything on.

flowchart TB
    subgraph you["You, in a browser"]
      B["Desktop · phone · tablet<br/><i>same session, several devices at once</i>"]
      SH["Share link<br/><i>read-only or writable, expires</i>"]
    end

    G["<b>Gateway</b> — one Docker container<br/><i>passkeys · 2FA · step-up on flagged hosts</i><br/><i>audit log · encrypted backups · alerts</i>"]

    subgraph reach["Three ways to reach a machine"]
      A["<b>Agent</b><br/><i>one Python file, dials OUT</i><br/><i>nothing to expose, works behind NAT</i>"]
      SSHH["<b>SSH</b><br/><i>nothing installed on the target</i>"]
      TEL["<b>Telnet</b><br/><i>switches, PDUs, old gear</i>"]
    end

    subgraph get["What you can do once you are there"]
      T["<b>Persistent sessions</b> (tmux)<br/><i>survive the browser, the gateway, the agent</i><br/><i>replay history · search · commands as objects</i>"]
      FS["<b>Files</b><br/><i>browse · edit · upload/download · git panel</i>"]
      SER["<b>Serial console</b><br/><i>RS232/RS485/USB on the host</i>"]
      FWD["<b>Port forwarding</b><br/><i>an internal web UI on its own subdomain</i>"]
      RUN["<b>Run on hosts</b><br/><i>one command → many hosts</i><br/><i>metrics · alerts · diagnostics</i>"]
    end

    B <-->|WebSocket| G
    SH -.->|watch or type| G

    G <-.->|"outbound WebSocket<br/>signed updates"| A
    G -->|dials out| SSHH
    G -->|dials out| TEL

    A --> T
    A --> FS
    A --> SER
    A --> FWD
    A --> RUN
    A -.->|"telnet bastion<br/><i>from inside the network</i>"| TEL

    SSHH --> T
    SSHH --> FWD
    TEL --> T
Loading

Warning

Anyone who gets past the login gets, on every host, the access of the user its agent runs as — like handing over an SSH key for that user. Roles (3.6) limit which hosts an account reaches and whether it gets a shell there, not what a shell can do; the gateway remains a single point of total compromise. Use a domain with HTTPS and passkeys, and keep the default unprivileged webterm agent user. Read Security and docs/THREAT-MODEL.md before exposing it.

Screenshots

Fictional demo fleet, captured on v3.5.8 with scripts/screenshots/run.sh. The images follow your GitHub theme (dark or light); the terminal itself stays dark in both.

Dashboard: Security card, sessions to resume and the fleet grouped by folder
A session with the Commands panel Run on hosts, results per host
Terminal session with the Commands panel: exit code and duration of every command Run on hosts: one saved command on four hosts, exit code and output per host
The host page Files, with multi-select
Host page: metrics, live session previews, security and agent status Files panel next to the terminal, three files selected for a bulk action
Settings → Sign-in & 2FA The editor (Monaco)
Settings, Sign-in & 2FA tab: connected devices, passkeys, 2FA Monaco editor open on a shell script on the host

A session on a phone, with the two-row key bar

Why WebTerm

  • The session outlives everything above it. It is a tmux session on your machine: close the tab, restart or upgrade the gateway, kill the agent, switch to a phone — it is still there, re-adopted by name.
  • Nothing to expose on your servers. The agent is one stdlib-only Python file that dials out over WebSocket with a token bound to that machine; the gateway stores no login for it. It needs Linux and Python 3.6+ — on hosts with an older or no python3 (Ubuntu 16.04, Debian 9) the installer adds a pinned, checksum-verified portable Python 3.12 for the agent alone; tmux is recommended. Supported hosts.
  • SSH and telnet when you cannot install anything — a switch, a customer's box — with the honest trade that those sessions live in the gateway and end when it restarts.
  • More than a shell: files, an editor, Docker, systemd, database consoles, port forwards, one command on many hosts, pending OS updates — from the same pane, phone included.
  • Built to sit on the internet: passkeys, 2FA step-up per host, signed agent updates, an audit log, encrypted backups, and a CI chain that blocks a broken image from shipping.

The long version, with the reasoning: docs/FEATURES.md.

Quick start

On a fresh Ubuntu/Debian server (amd64) with a domain pointing at it and ports 80/443 free:

git clone https://github.com/sm26449/webterm.git
cd webterm
git checkout v3.6.12          # the release tag from the version badge; a tag cannot move under you
less install.sh             # it is meant to be read: it runs as root
sudo ./install.sh --domain term.example.com --email you@example.com

The installer sets up Docker, the Traefik + WebTerm stack in /opt/webterm, a Let's Encrypt certificate (HTTP-01 by default; add --cf-token for Cloudflare DNS-01 and a wildcard for port-forward subdomains), the firewall and a daily encrypted backup — then prints the URL and a setup token. Open the URL, create the first account with that token, and add a passkey in Settings → Sign-in & 2FA.

Then + Add host in the sidebar gives you a one-line install command for the agent: run it on the server, and the host comes online. By default it creates a dedicated, unprivileged webterm user and runs as that.

Other routes, all in docs/INSTALL.md:

  • Already have Docker? cp .env.prod.example .env, fill it in, ./deploy.sh — details.
  • Build from source, ARM, or an IP for testing: ./setup.sh term.example.com (or ./setup.sh 192.168.1.10) — details.
  • Single sign-on with Authentik (bundled or your own) — details.
  • Putting agents on hosts: many at once, OS support, what to grant the webterm user, and how to remove it — details.

Features

One line per feature; the guides have the depth, and docs/FEATURES.md has everything in one page.

Terminal & sessions

  • Persistent tmux sessions with titles, notes and searchable history, open on several devices at once — how persistence works
  • Replay closed sessions (player or plain text) — session lifecycle
  • Commands as objects (OSC 133): exit code, duration and output per command, jump between them — docs/SHELL-INTEGRATION.md
  • Named split views (2–4 sessions), broadcast typing, popout windows — docs/design/SPLIT-VIEWS.md
  • Share links, read-only or writable, expiring and revocable — docs/FEATURES.md
  • Keyboard-first: command palette, parametrized snippets, a full shortcut map — docs/SHORTCUTS.md
  • A phone-friendly UI with a two-row key bar, installable as a PWA; English and Romanian

Hosts & infrastructure

Files & transfers

  • A Files panel that follows the terminal's cd, with multi-select, bulk download/delete and copy to another host — docs/TRANSFERS.md
  • Resumable uploads with an integrity check, folder downloads as .tgz, one Transfers widget — docs/TRANSFERS.md
  • Drop a file on the terminal, or paste a screenshot, and its path is typed at the prompt — docs/TRANSFERS.md
  • A Monaco (VS Code) editor and a Git panel — docs/FEATURES.md

Security

Operations

  • Email and webhook alerts (security events, resource thresholds) — docs/ALERTS.md
  • Encrypted backups from the UI, scheduled, off-host to Drive/Dropbox/SFTP/FTPS — docs/INSTALL.md
  • One-command upgrade with automatic rollback — Upgrade and rollback
  • Recovery procedures, including when the UI is unreachable — docs/RUNBOOK.md

Security

Hardened for public exposure: argon2 passwords and passkeys, a single-use setup token, brute-force lockout on the real client IP, __Host- cookies, Origin checks on every WebSocket, CSP and HSTS. Hosts can require a 2FA step-up to connect and lock their terminals on inactivity; a command guardrail can confirm or block dangerous commands, server-side. What you type is never recorded — transcripts hold output only.

Roles, honestly. Since 3.6 every account has role bindings — Owner, Admin, Operator or Viewer over all hosts, a folder, a tag or a single host (docs/ROLES.md). The gateway enforces which hosts an account can see at all and whether it gets a shell there; whoever has a shell on a host has the files of that agent's user, like SSH. Upgrading changes nothing for existing accounts (they become Owners). That is why the defaults are a dedicated unprivileged agent user, passkeys, and a domain with HTTPS.

Agent updates are Ed25519-signed with a key your gateway generates on first boot, so your fleet trusts only your key. Keep an offline backup of data/agent-signing.key: without it, deployed agents accept no more updates.

Configuration

Everything lives in .env next to the compose file (.env.prod.example for an image deploy; install.sh writes it for you). The ones you are most likely to touch:

Variable Role
WEBTERM_PUBLIC_URL public URL used by browsers, agents and passkeys, e.g. https://term.example.com
WEBTERM_DOMAIN the domain the TLS certificate is issued for
LETSENCRYPT_EMAIL contact for Let's Encrypt (image deploy)
CF_DNS_API_TOKEN optional Cloudflare token: DNS-01 instead of HTTP-01, and a wildcard for forwards
WEBTERM_ALERT_WEBHOOK Slack/Discord/Teams or any JSON endpoint for alerts (also in Settings → Notifications)
WEBTERM_SMTP_HOST / WEBTERM_ALERT_TO email alerts; without an SMTP host they are off
WEBTERM_OIDC_ISSUER / _CLIENT_ID / _CLIENT_SECRET single sign-on; off until all three are set (docs/SSO.md)
WEBTERM_TRUSTED_PROXY_HOSTS the proxy allowed to set X-Forwarded-For (compose sets it for you)

The full reference — every variable, secrets as _FILEs, agent-side and backup-script settings: docs/CONFIGURATION.md.

Back up the webterm-data volume, and above all agent-signing.key in it; the archive's passphrase and .env are not in it. Backups from the UI and the scheduled timer: docs/INSTALL.md; the rebuild checklist: docs/RUNBOOK.md.

Upgrade and rollback

cd /opt/webterm && sudo ./upgrade.sh     # latest release; or pass a tag: sudo ./upgrade.sh v3.6.12
./rollback.sh                           # back to the previous image, any time afterwards

upgrade.sh takes a backup, pulls the image and pins it by digest, syncs the scripts that run on the host, and hands off to deploy.sh, which rolls back by itself if the new container does not become healthy. With cosign installed and WEBTERM_COSIGN_IDENTITY set, it verifies the image signature first. WebTerm never updates itself; the UI only tells you a release exists. Details: docs/INSTALL.md; when things go wrong: docs/RUNBOOK.md.

Contributing and tests

make test         # the hermetic suite — exactly what CI gates the image on
make test-local   # + the suites that need a real tmux/agent (sandboxed from production)

Before an image is published, CI runs the unit suite, ruff, gitleaks and pip-audit, verifies the agent signature, boots the image, and drives it in a browser: an E2E run with a real agent on tmux (scripts/e2e-session.mjs, 202 checks), file and port-forward tests, a mobile audit on 10 devices and an accessibility gate. Building from source, the test layout and the full chain: docs/DEVELOPMENT.md. How to contribute — including re-signing the agent and regenerating these screenshots — is in CONTRIBUTING.md.

License

MIT · history in the CHANGELOG.

Built by Stefan Maldaianu, with development assistance from Claude (Anthropic).

About

Self-hosted web terminal & SSH bastion for your servers: persistent tmux sessions from any browser or phone. Outbound-only signed agent (no open ports), passkeys/SSO, files, port forwarding, Docker, audit log.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages