Repository navigation
Reverse Proxy and Subfolder Deployment
WebSSH uses regular HTTP routes plus Socket.IO/WebSocket traffic. A reverse proxy must preserve the public origin and support connection upgrades.
For one trusted proxy layer:
CORS_ORIGINS=https://ssh.example.com
SESSION_COOKIE_SECURE=true
TRUSTED_PROXIES=1TRUSTED_PROXIES is the number of trusted forwarding layers, not a Boolean.
Keep the WebSSH backend reachable only through those layers.
location / {
proxy_pass http://webssh:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}labels:
- "traefik.enable=true"
- "traefik.http.routers.webssh.rule=Host(`ssh.example.com`)"
- "traefik.http.routers.webssh.tls.certresolver=letsencrypt"
- "traefik.http.services.webssh.loadbalancer.server.port=5000"ssh.example.com {
reverse_proxy webssh:5000
}Apache httpd 2.4.47 or newer can proxy HTTP and WebSocket upgrades through
mod_proxy_http. Enable mod_proxy, mod_proxy_http, mod_headers, and
mod_ssl, then use a TLS virtual host such as:
<VirtualHost *:443>
ServerName ssh.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/ssh.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/ssh.example.com/privkey.pem
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
ProxyPass "/" "http://127.0.0.1:5000/" upgrade=websocket
ProxyPassReverse "/" "http://127.0.0.1:5000/"
</VirtualHost>Older Apache versions require mod_proxy_wstunnel and an explicit WebSocket
rule. Prefer a supported 2.4.47+ release so HTTP and upgrade traffic share the
same mapping.
For a public URL such as https://server.example.com/webssh, configure:
APPLICATION_ROOT=/webssh
TRUSTED_PROXIES=1
CORS_ORIGINS=https://server.example.com
SESSION_COOKIE_SECURE=trueThe reverse proxy must strip /webssh before forwarding the request and send
the original prefix as X-Forwarded-Prefix.
location /webssh/ {
proxy_pass http://webssh:5000/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Prefix /webssh;
}The trailing slash in both location and proxy_pass is intentional.
labels:
- "traefik.enable=true"
- "traefik.http.routers.webssh.rule=Host(`server.example.com`) && PathPrefix(`/webssh`)"
- "traefik.http.middlewares.webssh-strip.stripprefix.prefixes=/webssh"
- "traefik.http.middlewares.webssh-prefix.headers.customrequestheaders.X-Forwarded-Prefix=/webssh"
- "traefik.http.routers.webssh.middlewares=webssh-strip,webssh-prefix"
- "traefik.http.services.webssh.loadbalancer.server.port=5000"server.example.com {
handle_path /webssh/* {
reverse_proxy webssh:5000 {
header_up X-Forwarded-Prefix /webssh
}
}
}Use Apache httpd 2.4.47+ with mod_alias, mod_proxy, mod_proxy_http,
mod_headers, and mod_ssl. This is a complete TLS virtual host; replace the
hostname and certificate paths for your installation:
<VirtualHost *:443>
ServerName server.example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/server.example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/server.example.com/privkey.pem
ProxyRequests Off
ProxyPreserveHost On
ProxyAddHeaders On
ProxyTimeout 3600
RequestHeader unset X-Forwarded-For
RequestHeader set X-Forwarded-Host "server.example.com"
RequestHeader set X-Forwarded-Proto "https"
RequestHeader set X-Forwarded-Prefix "/webssh"
RedirectMatch permanent "^/webssh$" "/webssh/"
ProxyPass "/webssh/" "http://127.0.0.1:5000/" upgrade=websocket timeout=3600
ProxyPassReverse "/webssh/" "http://127.0.0.1:5000/"
</VirtualHost>Apache adds the directly connected client address after removing untrusted
incoming forwarding values. This example assumes one proxy layer. Do not add
another catch-all ProxyPass "/" from the root example to this virtual host.
This example terminates TLS at HAProxy and forwards to one WebSSH instance. Use the application settings above and a PEM containing the certificate chain and private key at the configured path. HTTP/1.1 supports both Socket.IO polling and its WebSocket upgrade through the same backend:
global
maxconn 1024
defaults
mode http
timeout connect 5s
timeout client 60s
timeout server 60s
timeout tunnel 1h
frontend webssh_https
bind :443 ssl crt /etc/haproxy/certs/server.example.com.pem alpn http/1.1
acl webssh_bare path /webssh
acl webssh_path path_beg /webssh/
acl has_query query -m len gt 0
http-request redirect code 308 location /webssh/?%[query] if webssh_bare has_query
http-request redirect code 308 location /webssh/ if webssh_bare !has_query
http-request deny deny_status 404 unless webssh_path
default_backend webssh
backend webssh
http-request set-header X-Forwarded-For %[src]
http-request set-header X-Forwarded-Host %[req.hdr(Host)]
http-request set-header X-Forwarded-Proto https
http-request set-header X-Forwarded-Prefix /webssh
http-request set-path %[path,regsub(^/webssh/,/)]
server webssh 127.0.0.1:5000set-path changes only the path; Socket.IO query parameters such as EIO,
transport, and sid remain intact. Do not replace the complete URI or route
WebSocket upgrades through a different prefix mapping. Forwarding headers are
overwritten because this example has exactly one trusted proxy layer.
For /tools/webssh, replace every /webssh prefix in the selected example
and set APPLICATION_ROOT=/tools/webssh. CORS_ORIGINS remains the public
origin without a path. /webssh-other is not part of /webssh/.
Start validation with exactly one Gunicorn gthread worker, as in the
Dockerfile. Keep the existing cookie and authentication settings. A working
health check alone does not establish that authenticated Socket.IO works.
These configurations must be syntax-checked against the installed proxy version and verified with the authenticated checks below before deployment. References: Apache ProxyPass and HAProxy configuration manual.
When the proxy runs in Docker:
- Attach WebSSH and the proxy to a private shared network.
- Remove public port publishing from WebSSH.
- Proxy to
webssh:5000over the private network. - Set
TRUSTED_PROXIESto the real number of trusted layers.
Do not publish 5000:5000 in parallel with the HTTPS proxy. That would allow
clients to bypass TLS and possibly the trusted-proxy boundary.
If login works but terminal activity disconnects or never starts:
- confirm HTTP/1.1 on the upstream connection;
- confirm
UpgradeandConnectionforwarding; - inspect browser network requests to
/socket.io/; - check that the proxy timeout accommodates long-lived connections;
- verify that the prefix is applied consistently for both HTTP and Socket.IO;
- verify
CORS_ORIGINSexactly matches the browser-visible origin, including the scheme and non-default port.
WebSSH uses forwarded addresses only within the configured proxy trust depth. An incorrect value can either record the proxy address instead of the client or trust attacker-supplied forwarding headers. Prefer a simple topology and keep the backend network private.
curl -I https://ssh.example.com/
curl -fsS https://ssh.example.com/health
curl -fsS https://ssh.example.com/readyFor a subfolder:
curl -I https://server.example.com/webssh/
curl -fsS https://server.example.com/webssh/health
curl -fsS https://server.example.com/webssh/readyComplete the check in a browser by logging in, opening a terminal, resizing it, and transferring a small file.
Log in normally, then inspect the browser Network panel for the public path
/webssh/socket.io/?EIO=4&transport=polling (or your configured prefix). A
successful initial polling response starts with an Engine.IO open packet (0).
When upgrading, the WebSocket request should receive 101 Switching Protocols.
An anonymous curl request can legitimately be rejected: WebSSH requires an
authenticated session before admitting an Engine.IO transport.
For a failed request, record the status, sanitized response text, transport, timestamp, and matching server/proxy log event. A 404 can indicate a prefix mapping error; a 400 can indicate an origin or protocol error; a 401 can indicate an admission rejection. None of these codes alone proves the cause. Do not share cookies, authorization headers, credentials, session IDs, or unredacted HAR files.
Check password, configured second factors and identity providers, session expiry and return to the original page, terminal input/resize, upload/download, idle connections and reconnect. Compare HTTP and Socket.IO scheme, host, prefix, and client address. Record the WebSSH revision and proxy configuration with results.
WebSSH documentation - Project - Issues - Discussions - Security
This Wiki is published from the versioned docs/wiki source. Please propose documentation changes through a pull request.
WebSSH Wiki
Install and operate
- Installation from Source
- Docker and Docker Compose
- Production Deployment
- Reverse Proxy and Subfolder Deployment
- Upgrading, Rollback, and FAQ
Identity and access
- Users and Account Management
- Authentication Overview
- GitHub Authentication
- LDAP and Active Directory
- OpenID Connect
- Passkeys and Recovery Codes
SSH and files
- SSH Connections and Host Keys
- Profiles, Jump Hosts, and Commands
- Terminal and Persistent tmux Sessions
- SFTP File Workspace and Transfers
- Tailscale SSH
Reference and administration