We
Networking Azure

WebSSH 1.6.3 on Ubuntu 24.04 on Azure User Guide

| Product: WebSSH 1.6.3 on Ubuntu 24.04 LTS on Azure

Overview

This guide covers the deployment, secure configuration and day-to-day use of WebSSH 1.6.3 on Ubuntu 24.04 on Azure using cloudimg Azure Marketplace images. WebSSH is an open-source web-based SSH client: it serves a browser terminal, built on Tornado and xterm.js, where you enter a target host, port, username and a password or private key, and it opens a real SSH session to that host and streams it back to the page. It lets you reach a shell from any modern browser with no local SSH client installed.

WebSSH is an SSH relay, and this image is built to ship it safely. By default the upstream wssh server binds 0.0.0.0:8888 with no authentication, so anyone who reaches the page can attempt an SSH connection to any host and port they type. That is an open pivot, SSRF and brute-force-relay surface. The cloudimg image closes it by design:

  • WebSSH is bound to loopback only (127.0.0.1:8888) — it is never a public listener.
  • nginx is the only public listener (TCP 80, and optionally 443), and it gates the entire UI and the terminal WebSocket behind per-VM HTTP Basic Auth.
  • The password is generated uniquely on first boot and written to /root/webssh-credentials.txt (mode 0600, root only). No shared or default password is ever baked into the image — the shipped image carries only a fail-closed placeholder that denies everyone until first boot mints the real one.

The Security section below is the most important part of this guide: because WebSSH can connect outward to anything you type, you are responsible for restricting who can reach the front door and where it may connect. Read it before you expose the instance.

What is included:

  • WebSSH 1.6.3 (official huashengdun/webssh, MIT licence) in a dedicated Python virtual environment at /opt/webssh/venv

  • webssh.service systemd unit running wssh bound to 127.0.0.1:8888 (loopback only), as a dedicated webssh system user

  • nginx reverse proxy on TCP 80 fronting the UI + terminal WebSocket behind per-VM HTTP Basic Auth, plus an unauthenticated static /healthz for load-balancer probes

  • webssh-firstboot.service systemd oneshot minting a per-VM Basic Auth password (bcrypt .htpasswd) and proving the front-door gate on first boot

  • A fail-closed placeholder credential in the shipped image, so no known password is ever present

  • Tornado CSRF protection retained, --fbidhttp=false for the behind-nginx topology, --origin=same cross-origin protection

  • Ubuntu 24.04 LTS base with latest security patches applied at build time, unattended-upgrades armed

  • Azure Linux Agent for seamless cloud integration and SSH key injection

  • 24/7 cloudimg support with a guaranteed 24 hour response SLA

Prerequisites

  • Active Azure subscription, an SSH public key, and a VNet + subnet in your target region

  • Subscription to the WebSSH listing on Azure Marketplace

  • A clear view of which hosts you intend to administer through WebSSH and which client networks should be allowed to reach it — you will lock the instance down to those below

Recommended virtual machine size: Standard_B2s (2 vCPU, 4 GB RAM) is comfortable — WebSSH is a light, stateless gateway. For many concurrent terminals use Standard_B2ms or Standard_D2s_v5.

Step 1: Deploy from the Azure Portal

Search WebSSH in Marketplace, select the cloudimg publisher, and click Create. Configure the network security group as tightly as you can:

  • TCP 22 (SSH) — for administration, from your management network only.
  • TCP 80 (WebSSH UI, behind Basic Auth) — from your trusted client networks only, never 0.0.0.0/0 on a public instance.
  • TCP 443 — optional, if you put your own TLS certificate in front of nginx (recommended for production; see Security).

Step 2: Deploy from the Azure CLI

az group create --name webssh-rg --location eastus

az vm create \
  --resource-group webssh-rg \
  --name webssh-vm \
  --image <cloudimg-webssh-image-urn> \
  --size Standard_B2s \
  --admin-username azureuser \
  --generate-ssh-keys \
  --public-ip-sku Standard

Then restrict inbound access to your own management CIDR rather than the whole internet:

az network nsg rule create \
  --resource-group webssh-rg --nsg-name webssh-vmNSG \
  --name allow-webssh-ui --priority 1010 \
  --destination-port-ranges 80 --access Allow --protocol Tcp \
  --source-address-prefixes <your-mgmt-cidr>

Step 3: First boot and retrieving your per-VM password

On first boot webssh-firstboot.service mints a unique password for the admin front-door account, writes it to /root/webssh-credentials.txt, starts nginx, and proves the gate. SSH in as azureuser and read it:

sudo cat /root/webssh-credentials.txt

You will see the WEBSSH_URL, the admin username, and the per-VM WEBSSH_PASSWORD, along with the security notes below. Confirm the services are healthy and that WebSSH is bound to loopback only, with nginx the only public listener:

systemctl is-active webssh.service nginx.service
sudo ss -tlnp | grep -E ':8888|:80 '
curl -s -o /dev/null -w 'unauthenticated /healthz -> HTTP %{http_code}\n' http://127.0.0.1/healthz

WebSSH and nginx active, the firstboot sentinel present, WebSSH listening on loopback 127.0.0.1:8888 only with nginx on port 80, and the unauthenticated /healthz endpoint returning HTTP 200

The credentials file is mode 0600, and the image ships only a fail-closed bcrypt placeholder in /etc/nginx/.webssh.htpasswd — no plaintext password is ever stored in the image:

The per-VM credentials file /root/webssh-credentials.txt with the password masked, showing the WebSSH URL and the security notes, plus the fail-closed bcrypt placeholder that ships in the image

Step 4: Sign in and open an SSH session

Open WEBSSH_URL (http://<your-vm-public-ip>/) in a browser. Your browser will prompt for the front-door credentials — sign in as admin with the per-VM password from Step 3. You then see the WebSSH connect form:

The WebSSH connect form with fields for Hostname, Port, Username, Password, an optional private key and passphrase, a TOTP field, and Connect and Reset buttons

Enter the target host you want to reach: its hostname or IP, the SSH port (22 by default), the username, and either a password or an uploaded private key (with an optional passphrase and a TOTP code for 2FA servers).

The WebSSH connect form filled in with a target hostname, port, and username, ready to open an SSH session

Click Connect. WebSSH opens a real SSH session to that host and drops you into a live xterm.js terminal in the browser:

A live xterm.js browser terminal connected over SSH, showing the remote host login banner and an interactive shell prompt

The terminal is a full interactive shell — run commands exactly as you would in any SSH client:

The browser terminal running real commands (whoami, uname -a, echo, ls) against the remote host and showing their output, confirming a working end-to-end SSH session

Step 5: Security — you are running an SSH relay

WebSSH will attempt to connect to any host and port a signed-in user types. Treat the instance as what it is — an SSH relay — and control both ends.

The front door only opens with the per-VM password

The whole UI and the terminal WebSocket sit behind per-VM HTTP Basic Auth. Without the password the front door is closed to everyone; a wrong password is rejected; only the per-VM password reaches the terminal. You can prove this from the VM:

PW=$(sudo grep '^WEBSSH_PASSWORD=' /root/webssh-credentials.txt | cut -d= -f2-)
curl -s -o /dev/null -w 'no password    -> HTTP %{http_code}\n' http://127.0.0.1/
curl -s -o /dev/null -w 'wrong password -> HTTP %{http_code}\n' -u admin:wrong-pw http://127.0.0.1/
curl -s -o /dev/null -w 'per-VM pw       -> HTTP %{http_code}\n' -u "admin:$PW" http://127.0.0.1/

Proof of the per-VM Basic Auth gate: an unauthenticated request returns HTTP 401, a wrong password returns HTTP 401, and only the correct per-VM password returns HTTP 200

Restrict WHO can reach it (inbound)

  • Keep the per-VM password secret; rotate it if it is ever shared (see below).
  • Lock inbound TCP 80/443 to trusted source IPs in the Azure NSG, or keep the instance on a private network reachable only over a VPN or Bastion. Do not expose it wide-open to 0.0.0.0/0.

Restrict WHERE it can connect (outbound / egress)

By design WebSSH lets the browser choose the destination, so an unrestricted instance can be used to reach anything routable from the VM. Constrain egress to only the hosts you administer. An Azure NSG outbound rule is the cleanest control; on the VM itself you can also use UFW, for example allowing SSH only to a specific management subnet:

sudo ufw default deny outgoing
sudo ufw allow out to <your-mgmt-cidr> port 22 proto tcp
sudo ufw allow out 53
sudo ufw enable

Encrypt the front door for production

The image serves the UI over plain HTTP on port 80 by default. For any production or internet-facing use, put your own TLS certificate in front of nginx (port 443) so the Basic Auth password and the terminal traffic are encrypted in transit — cloudimg ships nginx-ssl-certbot-ubuntu-24-04 as a one-step option, or add a listen 443 ssl server block with your certificate to /etc/nginx/sites-available/cloudimg-webssh.

Managing the service

Check status, follow logs, and restart:

systemctl status webssh.service --no-pager
sudo journalctl -u webssh.service --no-pager -n 20

To restart the stack after a configuration change, run sudo systemctl restart webssh.service nginx.service. To follow the logs live, use sudo journalctl -u webssh.service -f.

Rotate the front-door password by writing a fresh bcrypt entry (nginx reads the file per request, so no restart is needed):

NEWPW=$(openssl rand -base64 18)
sudo htpasswd -bB /etc/nginx/.webssh.htpasswd admin "$NEWPW"
echo "New WebSSH admin password: $NEWPW"

Store the new password somewhere safe; it replaces the one in /root/webssh-credentials.txt.

Verifying the install

Confirm the pinned versions and the whole stack, including a real SSH round-trip through the gateway:

/opt/webssh/venv/bin/wssh --version
nginx -v
systemctl is-active webssh.service nginx.service

Component and version inventory: WebSSH 1.6.3, nginx, Python 3, and the gateway self-test confirming a real authenticated SSH session round-trips through WebSSH

Troubleshooting

  • The browser never prompts for a password / the page is open — check that you are reaching the instance through nginx on port 80, not WebSSH's loopback :8888 directly. WebSSH must never be published on a public interface; confirm ss -tlnp | grep 8888 shows 127.0.0.1:8888 only.

  • "Connection refused" or a red status when you click Connect — WebSSH could not reach the target host/port, or your egress rules block it. Confirm the target is reachable from the VM (ssh <user>@<host>), and review your NSG/UFW outbound rules.

  • HTTP 401 with the right password — the password in /root/webssh-credentials.txt is the current one only if you have not rotated it. If you rotated it, use the new value.

  • Host key warning on connect — WebSSH uses the upstream default warning host-key policy (trust-on-first-use). Unknown target host keys are accepted with a logged warning, which you can review in journalctl -u webssh.service.

Support

Every cloudimg image includes 24/7 support with a guaranteed 24 hour response SLA. Contact support through the Azure Marketplace listing or at cloudimg.co.uk.