WeTTY 4.2.1
Overview
WeTTY is an open-source, MIT-licensed terminal in the browser. The page is a complete xterm.js terminal, so editors, pagers, colours, resizing and long-running commands behave as they do in a native terminal. Behind it, WeTTY opens every terminal as an SSH login, so a browser session is a real login on the server, with no desktop SSH client and no key file on the device you are using.
This guide covers launching and using the WeTTY 4.2.1 on Ubuntu 24.04 AMI from SecureAnyCloud on Amazon EC2. It is one version of the SecureAnyCloud Container Platforms and PaaS listing on AWS Marketplace, and you pick it as the software version when you subscribe and launch.
The browser terminal is a shell on your instance as its administrator account, with that account's sudo rights. Treat the browser password exactly as you treat your EC2 key pair. What you get:
- A password unique to your instance, from the first boot. nginx guards every route with HTTP Basic Auth (user
admin). The password is generated on your instance's first boot and stored only as a bcrypt hash for nginx and in a root-only file. Only the static/healthzanswers without it. If the password file is missing, unreadable, malformed or empty, nginx refuses to start and serves nothing. - Throttled password guessing. Requests that carry a password are limited to about 1 per second (60 a minute, bursts of up to 40) from each IPv4 address or IPv6 /64, not per account. nginx answers the excess with
429 Too Many Requestsbefore it checks the password, so a flood of wrong passwords from one address cannot slow down the terminals other people have open. Opening the terminal page uses about a dozen of those requests, and the open terminal then runs over one WebSocket. Allow WebSockets through any proxy between you and the instance. If a proxy blocks them, the terminal falls back to socket.io long-polling, which sends the password with every poll and can reach that limit, so the terminal stalls or disconnects with429errors. - WeTTY reachable only through nginx. WeTTY listens on a unix socket (
/run/wetty/wetty.sock), not on a TCP port. Only nginx and root can connect to it. Nothing but 22, 80 and 443 listens on the network. - A fixed account. Every terminal opens as the instance's default user (
ubuntu), with a per-instance SSH key that the instance accepts only from itself (restrict,pty,from="127.0.0.1,::1": no forwarding of any kind). Upstream WeTTY lets aRemote-Userheader or a/ssh/<user>address choose the account. nginx strips the header and fixes the address, so no request can open a shell as root or as anyone else, and WeTTY's remote host and remote command options are off. - Closed routes. Upstream WeTTY serves Prometheus metrics at
/wetty/metricswithout any sign-in. nginx passes only the terminal page, its static files and its terminal connection to WeTTY. Everything else, including/wetty/metricsand/wetty/ssh/<user>in any spelling, returns 404. - HTTPS from the first boot. nginx serves the terminal on port 443 with a certificate generated on your instance's first boot. Port 80 redirects to HTTPS. Requests for a host name the instance does not serve are dropped.
- Cross-site requests refused. Browsers send Basic Auth credentials with cross-site requests, so nginx refuses a terminal connection whose
Origin,RefererorSec-Fetch-Sitenames another site. WeTTY itself accepts only your instance's own address as an origin. - No password logins over SSH. SSH password and keyboard-interactive authentication are off for every account, and root cannot log in. No account has a password.
- An egress fence. WeTTY's own account may open one connection: SSH to this instance on loopback. The instance metadata service, your VPC and the internet are refused to it.
- No telemetry. WeTTY makes no outbound connection of its own. The build proves it by counting the service's outbound connections and DNS queries across a full workload, and there are none.
- Pinned, audited software. Node.js v22.23.4 LTS from the official tarball (checksum pinned against the release's signed checksum list) and wetty 4.2.1 with exactly its integrity-locked production dependency tree. Ubuntu 24.04 LTS is fully patched at build time, and unattended security upgrades are on.
WeTTY is an independent open-source project by Cian Butler and its contributors. This image is produced by SecureAnyCloud and is not affiliated with, endorsed by, or sponsored by the WeTTY project. It ships the free and open-source MIT-licensed software.
The image contains WeTTY 4.2.1 in /opt/wetty, Node.js v22.23.4 in /opt/node, nginx, OpenSSH, nftables, the licences and build provenance in /usr/share/doc/wetty, Ubuntu 24.04 LTS, cloud-init and the AWS Systems Manager agent.
Default user: ubuntu. Architecture: x86_64. Recommended instance type: m5.large.
Prerequisites
- An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet
- A security group that allows TCP 22 (SSH), TCP 443 (the browser terminal) and TCP 80 (redirects to HTTPS) from your own networks only
- m5.large (2 vCPU, 8 GiB) or larger. WeTTY itself is light: after a full workload, the build instance used about 85 MiB for WeTTY and 32 MiB for nginx. Size the instance for the work you will do in the terminal.
Launch the instance
In the AWS Marketplace listing, choose Continue to Subscribe, then Continue to Configuration. Pick WeTTY 4.2.1 on Ubuntu 24.04 as the software version, pick your Region, then choose Launch through EC2. Choose m5.large, your key pair, and a security group as described above.
To launch with the AWS CLI instead, replace the values in angle brackets:
aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
--instance-type m5.large --key-name <your-key-pair> \
--security-group-ids <your-security-group> --subnet-id <your-subnet> \
--metadata-options HttpTokens=required,HttpEndpoint=enabled \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=wetty}]'First boot takes under a minute. It generates the instance's password, WeTTY's SSH key and the certificate, and proves the access controls. Only then do WeTTY and nginx start.
Connect to your instance
The SSH user for WeTTY 4.2.1 on Ubuntu 24.04 is ubuntu. Connect with your key pair:
ssh -i <your-key.pem> ubuntu@<public-ip>Check that WeTTY answers
Check the service itself, not just the unit state. The commands below check the services, then the health endpoint, the sign-in challenge and the terminal page through nginx on HTTPS:
systemctl is-active wetty-egress-guard wetty nginx ssh
H=$(sudo sed -n 's#^WETTY_URL=https://\([^/]*\)/.*#\1#p' /root/wetty-credentials.txt)
PW=$(sudo sed -n 's/^WETTY_WEB_PASSWORD=//p' /root/wetty-credentials.txt) wt() { curl -s --cacert /etc/nginx/ssl/wetty.crt --resolve "$H:443:127.0.0.1" "$@"; }
C=$(wt -o /dev/null -w '%{http_code}' "https://$H/healthz")
A=$(wt -o /dev/null -w '%{http_code}' "https://$H/wetty")
T=$(wt -K <(printf 'user = "admin:%s"\n' "$PW") "https://$H/wetty" | grep -o '<title>[^<]*</title>')
V=$(/opt/node/bin/node -p 'require("/opt/wetty/node_modules/wetty/package.json").version')
echo "GET /healthz -> HTTP $C"
echo "GET /wetty without the password -> HTTP $A"
echo "GET /wetty with your password -> $T"
echo "WeTTY $V on Node.js $(/opt/node/bin/node --version)" [ "$C" = 200 ] && [ "$A" = 401 ] && [ "$T" = "<title>WeTTY</title>" ] && [ "$V" = 4.2.1 ] && echo WETTY_OKExpected output:
active active active active GET /healthz -> HTTP 200 GET /wetty without the password -> HTTP 401 GET /wetty with your password -> <title>WeTTY</title> WeTTY 4.2.1 on Node.js v22.23.4 WETTY_OKThe image also ships a round-trip check. It signs in over HTTPS exactly as a browser does, opens a real terminal session, runs a command in it and logs out:
sudo wetty-cred-roundtripExpected output:
OK https 301 444 healthz-200 anon-401 wrong-401 ui-200 metrics-404 ssh-user-404 csrf-sio-403 csrf-ws-403 terminal-session-ubuntu-ok logoutGet your URL and password
Read the credentials file. It is readable by root only:
sudo cat /root/wetty-credentials.txtThe file looks like this, but your values differ:
# WeTTY - this instance's browser terminal (sac, minted at first boot). Keep this file secret. # The terminal opens a shell as WETTY_SHELL_USER (with its sudo rights), so this password is as powerful as your EC2 key. WETTY_URL=https://ec2-<public-ip-dashed>.compute-1.amazonaws.com/wetty WETTY_WEB_USER=admin WETTY_SHELL_USER=ubuntu WETTY_TLS_SHA256_FINGERPRINT=<unique to your instance> # Change the password:
sudo wetty-set-password WETTY_WEB_PASSWORD=<24 characters generated on your instance>WETTY_URL uses the instance's public DNS name from EC2 instance metadata, and the certificate also names the public and private IP addresses. If you stop and start the instance and its public address changes, the next boot updates the name, the certificate, WeTTY's allowed origin and this file. To serve your own DNS name, see Step 8.
Sign in
Open WETTY_URL in your browser. The certificate is self-signed for your instance, so the browser warns once. Compare the fingerprint it shows with WETTY_TLS_SHA256_FINGERPRINT before you accept it.
The browser then asks for a user name and password. Enter admin and WETTY_WEB_PASSWORD. The terminal opens as ubuntu with the instance's message of the day.
Use the terminal
The session is a real interactive shell. Commands, full-screen programs, colours and resizing work as in a native terminal.
The gear icon at the top right opens WeTTY's settings (font, colours, scrollback), which are saved in your browser.
Type exit (or close the tab) to end the session. Each browser tab is its own SSH login, and who lists them.
Inspect the access model
The commands below show the listeners, the socket, the files that hold secrets, the key WeTTY uses and SSH's effective settings:
sudo ss -lntH | awk '{print $4}' | sed -E 's/.*[:]//' | sort -un | tr '\n' ' '; echo
sudo stat -c '%U:%G %a %n' /run/wetty/wetty.sock /root/wetty-credentials.txt /etc/nginx/.wetty.htpasswd /etc/nginx/ssl/wetty.key /var/lib/wetty/.ssh/id_ed25519
sudo cut -c1-33 /etc/ssh/sac-wetty/authorized_keys.ubuntu
sudo sshd -T | grep -E '^(passwordauthentication|kbdinteractiveauthentication|permitrootlogin) 'Expected output:
22 53 80 443 wetty:wetty 770 /run/wetty/wetty.sock root:root 600 /root/wetty-credentials.txt root:www-data 640 /etc/nginx/.wetty.htpasswd root:root 600 /etc/nginx/ssl/wetty.key wetty:wetty 400 /var/lib/wetty/.ssh/id_ed25519 restrict,pty,from="127.0.0.1,::1" permitrootlogin no passwordauthentication no kbdinteractiveauthentication noPort 53 is the instance's local DNS resolver on loopback (127.0.0.53 and 127.0.0.54). WeTTY's key is authorised in a root-owned file for ubuntu only, never in ~/.ssh/authorized_keys, so your own keys there are untouched.
Next, request the routes that upstream WeTTY would otherwise answer, using your password and a forged account header. The metrics and account-picking routes return 404, and a terminal opened with Remote-User: root still runs as ubuntu. The round-trip check from Step 3 proves the same with a real session:
H=$(sudo sed -n 's#^WETTY_URL=https://\([^/]*\)/.*#\1#p' /root/wetty-credentials.txt)
PW=$(sudo sed -n 's/^WETTY_WEB_PASSWORD=//p' /root/wetty-credentials.txt) wt() { curl -s --cacert /etc/nginx/ssl/wetty.crt --resolve "$H:443:127.0.0.1" -K <(printf 'user = "admin:%s"\n' "$PW") "$@"; } for p in /wetty/metrics /wetty/ssh/root /metrics /wetty/Metrics; do echo "GET $p -> HTTP $(wt -o /dev/null -w '%{http_code}' "https://$H$p")" done echo "socket.io from another site -> HTTP $(wt -o /dev/null -w '%{http_code}' -H 'Origin: https://example.com' "https://$H/wetty/socket.io/?EIO=4&transport=polling")"Expected output:
GET /wetty/metrics -> HTTP 404 GET /wetty/ssh/root -> HTTP 404 GET /metrics -> HTTP 404 GET /wetty/Metrics -> HTTP 404 socket.io from another site -> HTTP 403WeTTY's own account is fenced at the network layer. It may reach SSH on loopback and nothing else. Root on the same instance reaches the metadata service, which serves as the control:
I=$(sudo curl -s -m 4 -o /dev/null -w '%{http_code}' -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token)
W=$(sudo setpriv --reuid=wetty --regid=wetty --clear-groups curl -s -m 4 -o /dev/null -w '%{http_code}' -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 60' http://169.254.169.254/latest/api/token || true)
S=$(sudo setpriv --reuid=wetty --regid=wetty --clear-groups timeout 4 bash -c 'exec 3<>/dev/tcp/127.0.0.1/22' 2>/dev/null && echo OPEN || echo REFUSED)
echo "metadata service: root HTTP $I, wetty HTTP $W"
echo "SSH on loopback for wetty: $S" [ "$I" = 200 ] && [ "$W" = 000 ] && [ "$S" = OPEN ] && echo EGRESS_FENCE_OKExpected output:
metadata service: root HTTP 200, wetty HTTP 000 SSH on loopback for wetty: OPEN EGRESS_FENCE_OKUse your own DNS name and certificate
Point a DNS name at the instance, then make it the name that the terminal is served as. The command mints a new certificate for the name, moves WeTTY's allowed origin and updates the credentials file. The password does not change:
sudo wetty-set-hostname <your-domain>For a certificate that your browsers trust, replace /etc/nginx/ssl/wetty.crt and /etc/nginx/ssl/wetty.key (root only, mode 0600 for the key) with your own certificate and key for that name. Then run sudo nginx -t && sudo systemctl reload nginx. Running wetty-set-hostname again mints a new self-signed certificate over them.
Change the password
Generate a new password:
sudo wetty-set-passwordThis mints a new random 24-character password and writes it to /root/wetty-credentials.txt. It takes effect at once, and the old password stops working.
To choose your own password (24 to 128 letters and digits), pipe it in. This example pipes in a random one from openssl:
openssl rand -hex 16 | sudo wetty-set-password --stdinThe password is never placed on a command line or in a log. Your browser keeps the old password until you close the tab.
Review the image contents and licences
List the shipped documents and the build provenance:
ls /usr/share/doc/wetty cat /usr/share/doc/wetty/BUILD-PROVENANCE.txt tail -2 /usr/share/doc/wetty/dependency-licences.txtExpected output:
BUILD-PROVENANCE.txt LICENSE LICENSE.fontawesome LICENSE.nodejs app-manifest.sha256 client-bundle.sha256 client-bundle.txt dependency-licences.txt node-gyp.log node-manifest.sha256 npm-audit-summary.txt npm-audit.json wetty 4.2.1 (sac build) package: wetty@4.2.1 from registry.npmjs.org, integrity sha512-znt+GQf7mMZwgO6ad11Ye3B+BAQBCcEF+39YeqY0x1mhAj8IVy0kLOKh+w8GQToZnBVvGNqOzmyUe6pby36Upw== (MIT) dependencies: /opt/wetty/package-lock.json (sha256 6814f207a0c3109a380c15684d6bed63f583c72cbfdf746b6ee1f2d968721e90; 227 packages, sha512 integrity each, resolved with npm --before 2026-11-10), installed with npm ci --omit=dev --omit=optional --ignore-scripts (210 packages; no install script ran) node-pty 1.1.0 native addon: compiled on the build instance with npm's bundled node-gyp against the Node.js v22.23.4 headers (node-gyp.log); the temporary make + g++ were purged afterwards; node-pty's non-Linux prebuilds/ and its Windows-only payloads (third_party/conpty .dll/.exe, deps/winpty) removed; build/Release/pty.node is the only native object in /opt/wetty runtime: Node.js v22.23.4 (https://nodejs.org/dist/v22.23.4/node-v22.23.4-linux-x64.tar.xz, sha256 1805bcd35e6815559bd2eb97db438d3fdb0eb3a5e6a977b7c1badca76337e9ce, from the release's signed SHASUMS256.txt); npm, npx and corepack removed NOTE @fortawesome/free-solid-svg-icons@7.3.1 (CC-BY-4.0 AND MIT): icons CC BY 4.0 (attribution shipped), code MIT LICENCE-SCAN OK (210 packages)WeTTY is licensed under the MIT License. Its full text ships in /usr/share/doc/wetty/LICENSE, and Node.js's licence ships in LICENSE.nodejs. dependency-licences.txt lists the licence of each of the 210 installed packages, and all are permissive open-source licences (MIT, ISC, BSD, Apache 2.0, MIT-0).
The terminal page's icons are Font Awesome Free icons by Fonticons, Inc., licensed under CC BY 4.0 (code MIT). The attribution and licence ship in LICENSE.fontawesome.
The only compiled code in the WeTTY tree is node-pty's terminal addon, built from source on the build instance (node-gyp.log). npm-audit.json is the dependency audit at build time, and it reports no advisories.
Day-to-day operation
Check the service and the open sessions:
systemctl status wetty --no-pager | head -3 who- Restart. Restart WeTTY with
sudo systemctl restart wetty(open terminals end), and reload nginx withsudo systemctl reload nginx. - Logs. Use
sudo journalctl -u wettyfor WeTTY,/var/log/nginx/for requests, andsudo journalctl -u sshfor the SSH logins each terminal makes. - Persistence. Each terminal is a normal login. Run long-running work under
tmuxorscreen(or a systemd unit) so that it survives a closed tab. - Upgrades. Unattended upgrades keep Ubuntu, nginx and OpenSSH patched. WeTTY and Node.js are pinned.
Troubleshooting
- Nothing answers on port 443. nginx starts only after first boot has completed and only with a valid password file. Check
sudo systemctl status wetty-firstboot nginx --no-pagerandsudo journalctl -u wetty-firstboot --no-pager | tail -30. If first boot fails, it leaves nothing listening on purpose. - The browser keeps asking for the password. Use the user
adminand the password from/root/wetty-credentials.txt. Afterwetty-set-password, close the browser tab so that it forgets the old password. - The page loads but the terminal stays blank or disconnects at once. Open the page at the exact
WETTY_URLhost name, because WeTTY accepts only that origin. After a change of name, runsudo wetty-set-hostname <your-domain>so that WeTTY and nginx follow it.sudo systemctl status wetty ssh --no-pagershows whether WeTTY or SSH refused the connection. - The terminal stalls or drops after a while, and nginx's access log (
/var/log/nginx/access.log) shows429for/wetty/socket.io/requests from your address. Something between your browser and the instance (a corporate proxy or a VPN gateway) is blocking WebSockets. The terminal fell back to long-polling and reached the limit of about 1 password-bearing request per second (burst 40). Allow WebSocket upgrades to the instance through that proxy. - The browser warns about the certificate. This is expected until you install your own certificate. Compare the fingerprint with
WETTY_TLS_SHA256_FINGERPRINT. - HTTP 444 or an empty reply. The request used a host name the instance does not serve. Use the name in
WETTY_URL. - You removed or renamed the default user. The terminal opens the account that cloud-init created at first launch, recorded in
/etc/sac/wetty-shell-user. WeTTY refuses to start if that account is root or not a regular login account.
Security recommendations
- Allow ports 22, 80 and 443 in the security group from your own networks only. The browser terminal is a full shell with sudo.
- Change the password after your first sign-in (Step 9), keep
/root/wetty-credentials.txtreadable by root only, and use a password manager. - Use your own DNS name and a certificate that your browsers trust (Step 8).
- Keep the operating system patched. Unattended security upgrades are on.
Support and licensing
WeTTY is open source under the MIT licence, and there is no per-user licence fee. The AMI price covers SecureAnyCloud's hardening, first-boot setup and support.
- Email: sales@secureanycloud.com
- Website: https://secureanycloud.com
- Support: 24/7, with a 24-hour response SLA
Deploy on AWS
Launch WeTTY 4.2.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com