View and control remote terminals from your browser with end-to-end encryption
Originally written in Python, rewritten in Rust for single-binary distribution.
Features
- End-to-end encrypted with AES-128-GCM -- the server never sees plaintext
- Share unix terminals in real time
- Type from the terminal or browser; both are kept in sync
- Multiple browsers can connect simultaneously
- Read-only or read-write browser permissions
- Single static binary with frontend embedded, no runtime dependencies
- Terminal dimensions synced to the browser in real time
Installation
Quick Install
curl -fsSL https://raw.githubusercontent.com/cs01/termpair/main/install .sh | sh
Installs the latest binary to /usr/local/bin. Customize with environment variables:
INSTALL_DIR=~/.local/bin sh # custom install directory
VERSION=v0.5.0 sh # specific version
GitHub Releases
Download a prebuilt binary from the releases page. Available for Linux (x86_64, aarch64) and macOS (x86_64, Apple Silicon).
Build from Source
git clone https://github.com/cs01/termpair.git
cd termpair/termpair-rs
cargo build --release
cp target/release/termpair /usr/local/bin/
Usage
Start the server:
termpair serve
Share your terminal:
termpair share
This prints a URL containing a unique terminal ID and encryption key. Share it with whoever you want to give access. Anyone with the link can access your terminal while the session is running.
By default, termpair share runs your $SHELL. The server multicasts terminal output to all connected browsers.
How it Works
Server (termpair serve) -- Acts as a blind relay that routes encrypted WebSocket messages between terminal clients and browsers. It never has access to encryption keys or plaintext.
Terminal client (termpair share) -- Forks a pseudo-terminal (pty) running your shell. Reads pty output, encrypts it with AES-128-GCM, and sends it to the server via WebSocket. Decrypts incoming browser input and writes it to the pty.
Browser -- Connects via WebSocket, decrypts terminal output using the Web Crypto API, and renders it with xterm.js. User input is encrypted before sending.
Encryption
Three AES-128-GCM keys are created per session:
- Output key -- encrypts terminal output before sending to the server
- Input key -- encrypts browser input before sending to the server
- Bootstrap key -- delivered via the URL hash fragment (never sent to the server), used to securely exchange keys #1 and #2
Keys are rotated after 2^20 messages. IVs are monotonic counters to prevent reuse.
The browser must be in a secure context (HTTPS or localhost).
Deployment
NGINX
server 127.0.0.1:8000;
}
server {
server_name myserver.com;
listen 443 ssl;
ssl_certificate fullchain.pem;
ssl_certificate_key privkey.pem;
location /termpair/ {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://termpair_app/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
systemd
[Unit]
Description=TermPair terminal sharing server
After=network.target
[Service]
ExecStart=/usr/local/bin/termpair serve --port 8000
Restart=on-failure
RestartSec=1s
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable termpair.service
sudo systemctl restart termpair
TLS
Generate a self-signed certificate:
openssl req -newkey rsa:2048 -nodes -keyout host.key -x509 -days 365 -out host.crt -batch
Then pass it to the server:
termpair serve --certfile host.crt --keyfile host.key
CLI Reference
$ termpair serve [OPTIONS]
-p, --port port to listen on [default: 8000]
--host host to bind to [default: localhost]
-c, --certfile path to SSL certificate for HTTPS
-k, --keyfile path to SSL private key for HTTPS
$ termpair share [OPTIONS]
--cmd command to run [default: $SHELL]
-p, --port server port [default: 8000]
--host server URL [default: http://localhost]
-r, --read-only prevent browsers from typing
-b, --open-browser open the share link in a browser