Skip to main content

Web-based terminal: run any shell command in a PTY, accessed through the browser over WebSocket.

Project description

tunnelterm

A web-based terminal: spawn any shell command in a real PTY and interact with it from the browser over WebSocket.

  • Password-protected (per-IP rate limited)
  • HttpOnly + SameSite=Strict session cookie — JS never sees the token
  • Single-active-session per token
  • Fail-closed Origin allow-list required on non-loopback binds
  • Token TTL + LRU eviction
  • Sticky PTY sessions: page refresh keeps your shell alive
  • Themes, fonts, search, web-links, WebGL renderer
  • Persistent UI preferences in the browser

Install

uv tool install tunnelterm
# or
uv pip install tunnelterm

Run

--command is required. Examples:

TUNNELTERM_PASSWORD=hunter2 tunnelterm --command bash
TUNNELTERM_PASSWORD=hunter2 tunnelterm --command "zsh -l"
TUNNELTERM_PASSWORD=hunter2 tunnelterm --command htop --port 4242

Open http://127.0.0.1:4200 and log in with the password.

CLI options

Option Description Default
--command Command to run in the PTY. Required.
--host Host to bind to 127.0.0.1
--port Port to bind to 4200
--password-env Env var name to read the password from TUNNELTERM_PASSWORD
--config Path to TOML config file ~/.config/tunnelterm/config.toml
--allowed-origin Allowed Origin value (repeat). Required on non-loopback binds.
--allow-any-origin Disable Origin allow-list (unsafe; explicit escape hatch) off
--cookie-insecure Omit Secure flag on cookies (only needed for plain-HTTP non-loopback) auto (Secure on, off on loopback)
--enable-hsts Emit Strict-Transport-Security (HTTPS-only deployments) off
--session-idle-timeout Seconds to keep an unattached PTY alive before reaping 18000 (5h)
--log-level Log level: DEBUG, INFO, WARNING, ERROR INFO
--version Print version and exit

Environment variables

Variable Description
TUNNELTERM_PASSWORD Password for authentication (REQUIRED)
TUNNELTERM_HOST Bind host
TUNNELTERM_PORT Bind port
TUNNELTERM_COMMAND PTY command (if --command not given)
TUNNELTERM_ALLOWED_ORIGINS Comma-separated Origin allow-list
TUNNELTERM_SESSION_IDLE_TIMEOUT Idle timeout in seconds
LOG_LEVEL Log level fallback

Config file

~/.config/tunnelterm/config.toml:

password = "hunter2"
command = "bash"
host = "127.0.0.1"
port = 4200
allowed_origins = ["https://terminal.example.com"]
session_idle_timeout = 18000
enable_hsts = true

chmod 600 is recommended — tunnelterm logs a warning if the file is group- or world-readable.

Behind nginx with HTTPS (recommended for any non-loopback deployment)

Plaintext ws:// is fine for 127.0.0.1 only. For any other deployment, terminate TLS at a reverse proxy. Minimal nginx example:

server {
    listen 443 ssl http2;
    server_name terminal.example.com;

    ssl_certificate     /etc/letsencrypt/live/terminal.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/terminal.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:4200;
        proxy_http_version 1.1;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Upgrade           $http_upgrade;
        proxy_set_header Connection        "upgrade";
        proxy_read_timeout 1h;
    }
}

Then run tunnelterm with:

tunnelterm \
  --command bash \
  --allowed-origin https://terminal.example.com \
  --enable-hsts

Cookies will automatically get Secure set because the bind is loopback while the public scheme is HTTPS.

Systemd

sudo cp systemd/tunnelterm.service /etc/systemd/system/
# Edit User=, WorkingDirectory=, and ExecStart=.
sudo mkdir -p /etc/tunnelterm
sudo cp systemd/env.example /etc/tunnelterm/env
sudo chmod 600 /etc/tunnelterm/env
sudo $EDITOR /etc/tunnelterm/env  # set TUNNELTERM_PASSWORD

sudo systemctl daemon-reload
sudo systemctl enable --now tunnelterm
sudo journalctl -u tunnelterm -f

Session persistence

tunnelterm keeps your shell alive between connections so a page refresh, a brief network drop, or closing and reopening a tab does not throw you back into a fresh shell.

  • Each session token has one PTY bound to it on the server. Reconnecting with the same token attaches you to the existing shell.
  • On reattach the server replays the last 1 MB of PTY output, so the screen redraws to match what you were looking at.
  • "Keep me signed in" on the login page makes the cookie persistent (Max-Age = token TTL, default 24h). Unchecked, it's a session cookie that dies when the browser closes.
  • Sessions are reaped after --session-idle-timeout seconds of having no client attached (default 5h). The PTY is SIGKILLed.
  • Explicit logout (settings drawer → Logout) revokes the token, kills the PTY, and clears the cookie immediately.

Security model

Token storage

  • The session token is stored in an HttpOnly; Secure; SameSite=Strict cookie. JavaScript cannot read or exfiltrate it, even via XSS.
  • The browser carries the cookie automatically on /api/* POSTs and the /ws WebSocket upgrade.

CSRF / CSWSH

  • SameSite=Strict blocks the cookie on cross-site requests.
  • An additional Origin allow-list is enforced on every POST and WebSocket handshake. The server refuses to start on a non-loopback bind unless --allowed-origin is given (or --allow-any-origin explicitly opted into).

Brute force / abuse

  • 5 failed /api/auth attempts from one IP within 15 min → 5 min lockout.
  • /api/verify is rate-limited at 60 hits/IP/min.
  • WebSocket text frames are capped at 1 MiB to prevent memory exhaustion.

Lifecycle

  • Tokens expire after 24 h by default; LRU-evicted at 64 outstanding.
  • Each token may only be active in one WebSocket connection at a time.
  • Log lines include a SHA-256 fingerprint of the token, never the token itself.

HTTP hardening

  • CSP: default-src 'self'; script-src 'self'; connect-src 'self' etc. No wildcard ws/wss, no inline scripts.
  • X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, Permissions-Policy locking down hardware APIs.
  • Optional Strict-Transport-Security via --enable-hsts.
  • xterm assets are vendored locally with SRI hashes; no third-party CDN trust.

Development

uv sync
uv run pytest
uv run ruff check
uv run pyright

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tunnelterm-0.1.5.tar.gz (7.4 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tunnelterm-0.1.5-py3-none-any.whl (7.4 MB view details)

Uploaded Python 3

File details

Details for the file tunnelterm-0.1.5.tar.gz.

File metadata

  • Download URL: tunnelterm-0.1.5.tar.gz
  • Upload date:
  • Size: 7.4 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tunnelterm-0.1.5.tar.gz
Algorithm Hash digest
SHA256 c09a045510e7dff5ca9d3340e93adc70a31a20fe4591da000bb345fc4f1ce87e
MD5 693856a714496963d9157058a5b0e16a
BLAKE2b-256 ac53256fd818b6b45885f7620d3e41604b8710a7a3cd10215a64ce7489f4ad21

See more details on using hashes here.

Provenance

The following attestation bundles were made for tunnelterm-0.1.5.tar.gz:

Publisher: release.yml on nimitbhardwaj/tunnelterm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tunnelterm-0.1.5-py3-none-any.whl.

File metadata

  • Download URL: tunnelterm-0.1.5-py3-none-any.whl
  • Upload date:
  • Size: 7.4 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tunnelterm-0.1.5-py3-none-any.whl
Algorithm Hash digest
SHA256 71cf2c0b92115c886f61dbcd8107205bdcf916eebe56a39c7d576f2c08052a46
MD5 ef460a17076706969acae579ef69a19f
BLAKE2b-256 af1e7c9eb7b0f732cc04843d913f53fd38fc5f879e2b8d51370918684ca56f05

See more details on using hashes here.

Provenance

The following attestation bundles were made for tunnelterm-0.1.5-py3-none-any.whl:

Publisher: release.yml on nimitbhardwaj/tunnelterm

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page