WebSSH2 ships host key verification switched off by default
Web SSH Client using ssh2, socket.io, xterm.js, and express. webssh webssh2
At a glance
- What is it?
- A TypeScript gateway that proxies a WebSocket or Socket.io connection to an SSH2 server and serves an xterm.js terminal in the browser. Its own compatibility matrix disagrees with the client version its package.json pins, its container pinning example is still on release 2.3.2, and the MITM protection exists but is disabled until you turn it on.
- Who is it for?
- WebSSH2 fits a team that wants browser-based SSH with no client software and a container image it can pin, and the environment-variable configuration plus the Docker Hub mirror make that straightforward. Enable host key verification before you put it anywhere users reach SSH hosts through, since the default leaves every session open to a substituted key, and set WEBSSH2_AUTH_ALLOWED to the methods you actually intend to offer rather than leaving the permissive default.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 4, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The compatibility matrix says client 4.x, the dependency pins 5.4.2
The README carries a compatibility table mapping each server major to the browser client it ships with. Server 5.x is paired with client ^4.0.0, with a note that headerStyle and header.color were removed, tracked as an issue on the client repository. Server 4.x is paired with client ^3.x and credited with theming UI support from 3.7.0 onwards.
The package.json disagrees. Its webssh2_client dependency is written as 5.4.2, an exact version with no caret, so the client actually bundled with server 5.2.1 is a 5.x client, not the 4.x the table promises. The README's own explanation of when the matrix matters covers operators who self-build the client, link a local fork, or pin to a different client version. Nobody doing any of those is the case here: the mismatch is in the published server's own dependency.
Two other exact pins sit in the same list: ssh2 at 1.17 and better-sqlite3 at 12.9.0, both without a range, while express, socket.io, zod and the rest all take carets.
Host key verification is off by default on an SSH proxy
The one piece of security configuration the README gives in full is also the one it tells you to switch on yourself. Host key verification protects SSH connections against man-in-the-middle attacks by validating the public key the remote server presents, comparing it against a known-good key before the connection proceeds, using the same trust-on-first-use model as OpenSSH. The feature is disabled by default and must be explicitly enabled.
The configuration block lives under ssh in config.json rather than in an environment variable, and it has more moving parts than a boolean:
{
"ssh": {
"hostKeyVerification": {
"enabled": true,
"mode": "hybrid",
"unknownKeyAction": "prompt",
"serverStore": {
"enabled": true,
"dbPath": "/data/hostkeys.db"
},
"clientStore": {
"enabled": true
}
}
}
}There is a mode, an action for unknown keys, a server-side store with its own database path, and a client-side store. better-sqlite3 is the native module behind that host-key store, which is why the container build treats it specially.
The container build skips install scripts on purpose
The Dockerfile is unusually explicit about its dependency step. The install line is npm ci --omit=optional --audit=false --fund=false --ignore-scripts followed by npm rebuild better-sqlite3, and the comment above it explains why: lifecycle scripts are skipped so a compromised dependency cannot execute arbitrary code during the build. better-sqlite3 is singled out as the only runtime-critical native module, the host-key store, so its install script is re-run explicitly.
The same comment explains why the repository's own postinstall is left off. That script, also exposed as prepare:runtime, fetches a rollup native binary for bundlers, and the comment notes the build here is plain tsc, so anything it installs without saving is dropped by the runtime stage's npm prune and is deliberately not run.
The result is a real divergence between the two documented installs. The quick start tells you to run npm install --production locally, which does run postinstall and fetch that binary. The container does not, and gets its client from elsewhere.
The Docker pinning example still names release 2.3.2
Two registries are offered, with ghcr.io/billchurch/webssh2 as the preferred one and docker.io/billchurch/webssh2 as the mirror, both for linux/amd64 and linux/arm64. Running the default is docker run --rm -p 2222:2222 against the latest tag. Pinning is shown with a line-broken command and a release named webssh2-server-v2.3.2, mapped to the image tag 2.3.2, with the same tag said to be available in the legacy namespace.
The package version is 5.2.1. So the one example showing how to pin a release names a build from before two major version bumps, and it is the example most operators will copy when they want reproducibility. The release tags themselves are in the right shape for it, since the recent tags read webssh2-server-v5.2.1, webssh2-server-v5.2.0 and webssh2-server-v5.1.0, which is what a release tool with a manifest file at the root would produce.
The base image is pinned by digest rather than tag, node:22-alpine with a sha256, and a comment explains that the tag is kept alongside it for a dependency bot's benefit.
Authentication defaults to allowing every method
Configuration is environment-variable first, following the twelve-factor approach, and the short example sets a listen port, an SSH host, a header text and an authentication allowlist:
export WEBSSH2_LISTEN_PORT=2222
export WEBSSH2_SSH_HOST=ssh.example.com
export WEBSSH2_HEADER_TEXT="My WebSSH2"
# Allow only password and keyboard-interactive authentication methods (default allows all)
export WEBSSH2_AUTH_ALLOWED=password,keyboard-interactive
npm startThat comment is the important line. By default every authentication method the server supports is permitted, so the restriction is opt-in rather than opt-out. The container example sets the same variable to password,publickey and also passes WEBSSH2_SSH_ALGORITHMS_PRESET=modern, which narrows the algorithm set rather than the auth set.
Connection targets can also be narrowed per request. A path form carries the host and uses HTTP Basic auth for the gateway, and a query form sets port and terminal type, such as ssh?port=2244&sshterm=xterm-256color. Subnet restrictions exist as IPv4 and IPv6 CIDR validation.
The published file list asks for ChangeLog.md, the tree has CHANGELOG.md
The npm manifest lists four files to publish: dist, LICENSE, README.md and ChangeLog.md. Three of those exist under exactly those names in the repository. The changelog does not: the file at the root is CHANGELOG.md in capitals, and it is also the file a release automation manifest and a changelog at the root would keep writing to.
On a case-insensitive filesystem the difference never surfaces, which is why it survives review. On a case-sensitive one, the published tarball quietly has no changelog in it even though the manifest asked for one.
The same file carries a case inconsistency in its URLs, with homepage, repository url and bugs all pointing at billchurch/WebSSH2 while the repository is addressed as billchurch/webssh2 everywhere else in the project. It also carries an ignore field listing .gitignore, which is not one of the file-selection mechanisms npm uses, next to a files array that does the job.
Four tsconfigs, three eslint configs and two test runners
The root holds tsconfig.json plus tsconfig.build.json, tsconfig.eslint.json and tsconfig.js-check.json, so type checking, the production build, linting and JavaScript checking each get their own settings. Linting is split three ways as well: a base eslint.config.mjs, an eslint.config.complexity.mjs applied to app and scripts only, and an eslint.config.sonar.mjs, with sonar-project.properties alongside them and a matching suppression noted in the Dockerfile.
Tests run on two frameworks. The unit path builds first and then runs vitest, while playwright.config.ts covers the browser side, and the end-to-end script builds again before invoking it with a container runtime variable and a feature flag. There is a knip.json for detecting unused exports, a .husky/ directory for git hooks, .prettierrc.json with a markdownlint config, and a .trivyignore, which points at image scanning being part of the release rather than an afterthought.
Development runs through tsx watch on index.ts with NODE_ENV set and the webssh2 debug namespace enabled, which is the only place the source-level entry point appears, since start runs the compiled dist/index.js.
SFTP is annotated v2.6.0+ on a package at 5.2.1
The feature list carries version annotations, and only one of them is still meaningful. SFTP support for file transfer is marked v2.6.0 and up, while the package version is 5.2.1, so the marker describes a release line the project left behind. Telnet support is marked optional and disabled by default, the exec channel is described as running commands without opening a shell, and environment variables are passed through to the SSH session.
The documentation tree is larger than the README. DOCS/ splits into getting-started with a quick start, an installation guide, a Docker and Kubernetes page and a migration guide; configuration with an overview, an environment variable reference and a URL parameter reference; features covering authentication, private keys, the exec channel and environment forwarding; development with contributing and setup; an api/ directory for WebSocket and REST; build and container guides; and reference pages for troubleshooting and breaking changes.
The examples directory is concrete about deployment: an env example file, its own Dockerfile, a config.json sample, a compose file, an nginx directory and an SSO page written for BIG-IP APM.
Editorial conclusion
WebSSH2 fits a team that wants browser-based SSH with no client software and a container image it can pin, and the environment-variable configuration plus the Docker Hub mirror make that straightforward. Enable host key verification before you put it anywhere users reach SSH hosts through, since the default leaves every session open to a substituted key, and set WEBSSH2_AUTH_ALLOWED to the methods you actually intend to offer rather than leaving the permissive default. Verify two version claims before you deploy: the compatibility table says server 5.x pairs with client ^4.0.0 while the dependency pins webssh2_client 5.4.2, and the documented release-pinning example names 2.3.2 against a package version of 5.2.1. Node.js 22 or later is required. The last push was on 2026-08-18.
Frequently asked questions
What are the system requirements for running WebSSH2?
Node.js 22 LTS or later. From a clone you run npm install --production and npm start, then reach the terminal at http://localhost:2222/ssh, or at the telnet path if telnet support is turned on.
Is host key verification enabled in WebSSH2?
No. It is disabled by default and must be enabled explicitly in configuration, by adding a hostKeyVerification block under ssh in config.json. The block takes a mode, an action for unknown keys, and separate server and client stores.
Which authentication methods does WebSSH2 allow by default?
All of them. The default permits every supported method, and WEBSSH2_AUTH_ALLOWED is how you restrict it, for example to password,keyboard-interactive or to password,publickey as the container example does.
Where can I pull the WebSSH2 container image from?
From ghcr.io/billchurch/webssh2, which is the preferred registry, or from docker.io/billchurch/webssh2 as the Docker Hub mirror. Both publish linux/amd64 and linux/arm64 images and carry the same tags.
How do I run a WebSSH2 container with environment variables?
Pass them with -e on the run command, for example WEBSSH2_SSH_HOST, WEBSSH2_SSH_ALGORITHMS_PRESET=modern and WEBSSH2_AUTH_ALLOWED=password,publickey, alongside the port mapping. The default port is 2222 on both sides.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/billchurch-webssh2)