WeTTY on AWS User Guide
Overview
This guide covers launching and using the WeTTY 4.2.1 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg Container Platforms and PaaS listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.
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: no desktop SSH client, no key file on the device you are using.
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. On this image:
- 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. - Password guessing is throttled. 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 is 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. - The account is fixed. 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, is 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, and 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: 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 cloudimg and is not affiliated with, endorsed by, or sponsored by the WeTTY project. It ships the free and open source MIT licensed software.
What is on the image: 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 (measured on the build instance after a full workload: about 85 MiB for WeTTY and 32 MiB for nginx); size the instance for the work you will do in the terminal.
Step 1: 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 and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above.
With the AWS CLI (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, proves the access controls and only then lets WeTTY and nginx start.
Step 2: Connect to your instance
| Version | SSH user |
|---|---|
| WeTTY 4.2.1 on Ubuntu 24.04 | ubuntu |
ssh -i <your-key.pem> ubuntu@<public-ip>
Step 3: Check that WeTTY answers
Check the service itself, not just the unit state. 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_OK
Expected 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_OK
The image also ships a round trip check that 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-roundtrip
Expected 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 logout
Step 4: Get your URL and password
sudo cat /root/wetty-credentials.txt
The file is readable by root only. Your values differ:
# WeTTY - this instance's browser terminal (cloudimg, 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.
Step 5: 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:

Step 6: 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), saved in your browser:

Type exit (or close the tab) to end the session. Each browser tab is its own SSH login; who lists them.
Step 7: The access model
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/cloudimg-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 no
Port 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.
The routes upstream WeTTY would otherwise answer, with your password, and a forged account header: the metrics and account picking routes are 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 403
WeTTY's own account is fenced at the network layer: it may reach SSH on loopback, nothing else. Root on the same instance reaches the metadata service (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_OK
Expected output:
metadata service: root HTTP 200, wetty HTTP 000
SSH on loopback for wetty: OPEN
EGRESS_FENCE_OK
Step 8: Your own DNS name and certificate
Point a DNS name at the instance, then make it the name the terminal is served as. This mints a new certificate for it, moves WeTTY's allowed origin and updates the credentials file; the password does not change:
sudo wetty-set-hostname <your-domain>
For a certificate 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.
Step 9: Change the password
sudo wetty-set-password
This mints a new random 24 character password, writes it to /root/wetty-credentials.txt and takes effect at once; the old password stops working. To choose your own (24 to 128 letters and digits), pipe it in, for example a random one from openssl:
openssl rand -hex 16 | sudo wetty-set-password --stdin
The password is never placed on a command line or in a log. Your browser keeps the old password until you close the tab.
Step 10: What is in the image, and under what licence
ls /usr/share/doc/wetty
cat /usr/share/doc/wetty/BUILD-PROVENANCE.txt
tail -2 /usr/share/doc/wetty/dependency-licences.txt
Expected 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 (cloudimg 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 in LICENSE.nodejs. dependency-licences.txt lists the licence of each of the 210 installed packages: 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: no advisories.
Day to day operation
systemctl status wetty --no-pager | head -3
who
- Restart WeTTY with
sudo systemctl restart wetty(open terminals end) and reload nginx withsudo systemctl reload nginx. - Logs.
sudo journalctl -u wetty(WeTTY),/var/log/nginx/(requests) andsudo journalctl -u ssh(the SSH logins each terminal makes). - Persistence. Each terminal is a normal login; long running work should run under
tmuxorscreen(or a systemd unit) so 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:
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 it forgets the old one. - The page loads but the terminal stays blank or disconnects at once. Open the page at the exact
WETTY_URLhost name: WeTTY accepts only that origin. After a change of name runsudo wetty-set-hostname <your-domain>so WeTTY and nginx follow it.sudo systemctl status wetty ssh --no-pagershows whether WeTTY or SSH refused it. - 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, a VPN gateway) is blocking WebSockets, so 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. 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. - I removed or renamed the default user. The terminal opens the account cloud-init created at first launch, recorded in
/etc/cloudimg/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 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 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; there is no per user licence fee. The AMI price covers cloudimg's hardening, first boot setup and support.
- Email: support@cloudimg.co.uk
- Website: https://www.cloudimg.co.uk
- Support: 24/7, with a 24 hour response SLA