搞运维这些年,我遇到过最尴尬的场景之一,就是人在现场或临时换了台电脑,手头却没有装任何 SSH 客户端。Windows 自带命令行连原生 ssh 都得看版本,macOS 虽然自带,但如果现场设备是别人的,更不可能随便装软件。后来我用 WebSSH 把这个问题彻底解决了:服务器上跑一个轻量服务,浏览器就是终端,输个地址就能进 shell。这篇文章就从我的实战角度,聊聊 WebSSH 怎么选、怎么部署、怎么用才不踩坑。
1. WebSSH 到底解决了什么问题
1.1 一次"没有客户端"的现场经历
先交代一下背景。我之前维护着一套偏传统的业务集群,服务器散在客户机房和托管机房,平时用的工具主要是 Xshell、Tabby 这类本地终端,配合 VSCode 的 Remote SSH 做编辑器。这套组合在正常情况下很好用,但有几个场景会卡住:
- 借用别人的电脑,没法装软件,或者公司对终端软件有强管控。
- 现场给客户演示、排查问题时,客户侧只有一台带浏览器的办公电脑。
- 需要给临时同事开一个只读排查通道,不想让他碰本地密钥文件。
- 手机、平板临时连一下看看服务状态。
WebSSH 正好把这些场景全部覆盖掉:浏览器是操作系统自带的,不需要安装任何客户端;访问形式就是一个 URL,扔个链接就能用;配合认证和授权,可以精确控制对方能做什么。这不是什么黑科技,本质上是把 SSH 的交互能力搬到了 Web 端,核心协议流程是:浏览器页面通过 WebSocket 连到 WebSSH 服务,WebSSH 服务再把它转成标准 SSH 协议去连接目标服务器。也就是说,中间多了一层翻译,换来的是"零客户端、任意设备、随时可用"。
1.2 SSH over WebSocket 是怎么工作的
很多人第一次用 WebSSH 会好奇:浏览器里那个像模像样的终端到底是怎么画出来的?数据又是怎么走的?
浏览器端的终端界面,绝大多数方案都基于 xterm.js 这个前端组件。它负责模拟终端输入输出,让我们能看到光标、颜色、字符,并且把键盘按键转换成终端需要的字节流。而后端 WebSSH 服务,比如 ttyd、Wetty、sshwifty 这些,本质上做三件事:
- 在服务器上起一个 WebSocket 服务,浏览器通过
ws://或wss://连接上来。 - WebSocket 收到浏览器发来的字符流后,把它喂给系统的 PTY(伪终端)或直接交给 ssh 子进程。
- SSH 子进程的输出再反过来通过 WebSocket 推回浏览器渲染。
为什么不直接用 HTTP 轮询?因为终端是双向实时交互的,服务器要主动往浏览器推数据,HTTP 轮询又慢又费资源。WebSocket 是长连接双工通道,天然适合这种场景。这也是 WebSSH 调试时最容易出问题的地方:只要中间链路把 WebSocket 连接掐了,终端立刻就"死"给你看。后面 Nginx 反代部分我会强调这个细节。
1.3 典型适用场景:应急、跳板、协作
从我的实际使用来看,WebSSH 最适合四类场景:
- 临时应急运维。设备没客户端、人员是临时借调、操作只需要几分钟,WebSSH 是最快路径。
- 内部跳板机的入口。很多公司有跳板机或者堡垒机,把 WebSSH 挂在跳板机上,开发人员浏览器登录后可继续跳内网,比每个人本地装客户端好管理得多。
- 演示和协作。给客户演示排查过程,或者两个人一起看同一个终端输出,WebSSH 天然支持"分享 URL"这种协作方式。
- 移动端临时查看。手机浏览器打开页面,救急看一眼日志、执行一条重启命令,体验基本够用。
需要提醒的是,WebSSH 不适合当作唯一的主力终端。浏览器进程一旦被误关,会话就断了,不像 Tabby、Xshell 还能断线重连;浏览器本身也吃内存,长期跑交互式命令的体验不如本地客户端。我的用法是:应急和协作走 WebSSH,日常开发运维回到本地终端。
2. 主流的 WebSSH 方案怎么选
2.1 我碰过的几个开源方案
WebSSH 听起来是个小工具,生态里其实有不少实现。我自己试过或评估过的主要有四类:
| 方案 | 语言/技术栈 | 特点 | 部署难度 |
|---|---|---|---|
| ttyd | C | 轻量、无 Node 依赖、性能好 | 低 |
| Wetty | Node.js | 纯 npm 安装,配置直观 | 低 |
| sshwifty | Go | 功能全,自带用户管理和多连接管理 | 中 |
| webssh2 | Node.js | 支持多连接、文件上传下载 | 中 |
我第一次实际用的是 Wetty,npm 一条命令就能装,上手非常快。但用了半年后遇到两个问题:一是 Node 版本兼容要求比较苛刻,老服务器上装新 Node 反而麻烦;二是并发连接一多,内存占用会往上跳。后来切换到 ttyd,整体感受明显改善,我也一直用到现在。
2.2 为什么我把 ttyd 当主力
选 ttyd 有这几个原因:
- 轻。编译完只有一个二进制文件,没有运行时依赖,拷到任何 Linux 服务器上就能跑。这个特性在客户环境里特别省事,不动系统、不改全局软件源。
- 稳。ttyd 底层走 libwebsockets,WebSocket 的稳定性比我在 Wetty 上遇到的情况好不少,长时间挂机不容易断。
- 快。启动速度快、页面加载速度快,弱网环境下表现也还行。
- 参数直接。支持
-p、-i、--login-credential、--ssl这些命令行参数,写 systemd 服务时非常清晰。
还有一个关键点:ttyd 启动时可以直接指定要执行的命令,不一定是开一个系统登录 shell。比如我实例里让它执行ssh -i /path/key user@10.0.0.10,浏览器登录后直接就进了内网机器,使用者完全不需要关心内网 IP 和密钥在哪。这种"网页打开即登入"的体验,用来做临时访问和轻度协作非常舒服。
2.3 为什么不建议花精力自研
有段时间团队里考虑过要不要用 xterm.js + WebSocket 自己写一套。后来评估下来没有继续,原因很实在:终端这东西看着简单,真正项目里会有一堆边角,比如终端尺寸自适应、编码转换、特殊按键模拟、复制粘贴安全策略、多标签管理、审计日志对接,每一项都要时间打磨。ttyd 这类成熟方案已经把这些问题处理得差不多了,直接在它上面包一层鉴权和审计,性价比高得多。除非你是那种有特殊交互需求的大团队,否则我建议直接复用开源方案,把精力花在"怎么用好、怎么管好"上。
3. 实操:把 ttyd 部署到服务器
3.1 编译安装还是 Docker
如果你只是在自己可控的服务器上用,我强烈建议直接用 Docker:
docker run -it --rm -p 7681:7681 tsl0922/ttyd -- login一条命令就能起来,不污染宿主机环境,升级也简单。但要注意,容器方式有个隐含限制:默认容器里没有你宿主机上的工具链,进去之后是个干净的容器 shell,操作完要退出。如果你想通过 ttyd 再 SSH 到其他内网机器,或者需要直接操作宿主机,我更推荐编译安装。
编译步骤不复杂,以 Debian/Ubuntu 为例:
apt update apt install -y build-essential cmake libjson-c-dev libwebsockets-dev git clone https://github.com/tsl0922/ttyd.git cd ttyd mkdir build && cd build cmake .. make -j$(nproc) make install编译过程大约几分钟,取决于服务器性能。装好后执行ttyd --version验证。这里有个很典型的坑:如果服务器上的 libwebsockets 版本太旧,cmake 会报错找不到libwebsockets.h。解决办法是卸载旧版后从源码编译新版,或者直接换用较新的 Debian/Ubuntu LTS 系统。省心的做法是不要在一台很老的机器上折腾,选个较新发行版一次过。
3.2 配置 systemd 守护进程
编译安装完,建议用 systemd 管起来,保证服务重启后自动拉起。下面是我一直在用的配置:
[Unit] Description=ttyd SSH web client After=network.target [Service] ExecStart=/usr/local/bin/ttyd -p 7681 -i 127.0.0.1 --login-credential ops:webpass login Restart=always RestartSec=5s User=ops [Install] WantedBy=multi-user.target这里有两个关键点必须说明:
-i 127.0.0.1:只监听本机回环地址,不让公网直接访问 7681 端口,由前面的 Nginx 做统一入口。这一步是安全底线。--login-credential ops:webpass:登录 Web 界面时要输入用户名和密码,而不是打开页面就直接进 shell。第一次配置时很容易忽略这个参数,结果页面一打开就是 root shell,非常危险。
启动命令:
systemctl daemon-reload systemctl enable --now ttyd浏览器访问http://服务器IP:7681,输入上面设置的ops/webpass,就能在网页里看到终端了。这个状态已经可以日常用,但生产环境我还会加一层 HTTPS 和统一入口。
3.3 一个更灵活的用法:作为内网跳板
我实际使用中,ttyd 真正的价值不是连宿主机,而是作为一个"跳板终端",统一收敛到内网多台机器。思路是:ttyd 启动时不让它执行login,而是执行一个自定义 SSH 命令:
ExecStart=/usr/local/bin/ttyd -p 7681 -i 127.0.0.1 \ --login-credential ops:webpass \ ssh -o StrictHostKeyChecking=no ops@10.0.0.10这样浏览器打开的终端直接就落在内网服务器10.0.0.10的 shell 里,用户不需要知道内网 IP,也不需要接触 SSH 密钥。配合宿主机上放好的部署密钥,从浏览器到内网机器的完整链路就打通了。当然,这台宿主机自身的安全等级也要跟着提上去,因为它等于内网的闸门,越狱风险都在这一层。
如果有人需要访问多台内网机器,我一般会在前端放一个简单的 HTML 导航页,把不同跳板地址做成按钮,点击后打开对应 WebSSH 路径。这个比给每个人发一份密钥配置文档简单得多,也方便后续审计谁访问了哪台机器。
4. 日常使用中的几个提升体验的小配置
4.1 用 Nginx 反向代理并启用 HTTPS
直接走IP:端口虽然能用,但明文 HTTP 传输终端内容,等于把密码和操作记录放在网络上裸奔,正规环境肯定不行。我通常会在前面挂一层 Nginx,启用 HTTPS。
server { listen 443 ssl; server_name shell.example.com; ssl_certificate /etc/letsencrypt/live/shell.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/shell.example.com/privkey.pem; location / { proxy_pass http://127.0.0.1:7681; 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_read_timeout 3600s; proxy_send_timeout 3600s; } }这里最容易出问题的是 WebSocket 的Upgrade头。如果 Nginx 没传Connection: upgrade,浏览器页面能打开,但终端一直卡在"连接中",因为 WebSocket 握手失败。首次配置完务必在浏览器开发者工具里确认 Network 标签页有没有101 Switching Protocols的状态码。
证书申请用 certbot 的 webroot 或 standalone 模式都行,建议顺手加上 HTTP 到 HTTPS 的跳转。这样用户只记一个 HTTPS 地址,安全也有了。
4.2 会话超时、心跳与空闲断开
浏览器终端最让人头疼的问题之一就是"挂着挂着就断了"。ttyd 可以通过--max-clients限制并发数,但会话超时一般交给 Nginx 处理。如果你希望会话长期不闲置,把proxy_read_timeout调大即可;如果希望安全一点,可以设置为较短值,超时自动断开。
我更推荐在系统层面对 SSH 会话做约束,避免永久空闲连接占资源。比如在~/.ssh/config里设置:
Host * ServerAliveInterval 60 ServerAliveCountMax 3这样客户端每 60 秒发送一次心跳,如果服务器超过 3 次没有响应就断开。这个配置对 WebSSH 起到的是"兜底保活"作用,能有效避免一堆僵尸会话占用服务器资源。
4.3 与 VSCode Remote SSH 的配合
很多人问 WebSSH 和 VSCode Remote SSH 是不是替代关系。我的看法是互补。VSCode Remote SSH 的强项是把编辑器、调试器、插件都搬到远端,体验接近本地开发;WebSSH 的强项是零安装、任何设备可用、适合应急和轻量操作。
我自己的组合方式是:日常开发用 VSCode Remote SSH 连开发机,环境配好、插件装好,编辑代码效率直追本地;遇到临时排查、客户现场演示、或者手边只有手机的时候,就开 WebSSH。两边不冲突,反而互为备份。如果你在团队里同时维护多台开发机,还可以把 WebSSH 的地址整理成文档放在内部知识库,省去反复教同事配 SSH。
5. 常见问题与排查实录
5.1 浏览器打开页面,终端一直转圈
这是 WebSSH 踩坑率最高的问题。通常原因有三个:
- Nginx 反代没配好 WebSocket Upgrade 头,检查
proxy_set_header Connection "upgrade"。 - 浏览器与服务器时间偏差过大,导致 TLS 握手或 WebSocket 校验失败。校准系统时间:
timedatectl set-ntp true。 - ttyd 监听在 127.0.0.1,但有人直接用公网 IP 访问 7681 端口,没有走 Nginx,自然连不上。此时应确认访问入口是 HTTPS 域名,而不是原始端口。
排查思路:先在服务器本地curl http://127.0.0.1:7681看是否返回 HTML;再检查防火墙端口;最后看 Nginx 日志里的 upgrade 相关错误。一步步缩小范围,基本几分钟能定位。
5.2 中文乱码和退格键行为不对
浏览器终端里输中文或者看带中文的日志,偶尔会乱码。多数情况下是 locale 没设对。建议在 ttyd 启动命令里显式指定:
ExecStart=/usr/local/bin/ttyd -p 7681 -i 127.0.0.1 \ --login-credential ops:webpass \ env LANG=en_US.UTF-8 login注意我写的是env LANG=en_US.UTF-8 login,这比在 shell 里 export 更直接,能保证登录后的环境变量正确。
退格键行为不对通常是终端类型问题。ttyd 前端用的是 xterm.js,按理说应该支持正常退格。如果按退格变成^H,检查你的 shell 设置了正确的TERM=xterm-256color,并在.bashrc或.zshrc里加上stty erase ^H。这两个问题排查清楚后,浏览器终端的使用体感基本能接近本地客户端。
5.3 长时间不操作,终端会话断了
我试过在浏览器里挂着一个top,人去开会,回来发现页面重新连接或直接退出。问题的本质是中间链路断开,可能的原因有三类:
- Nginx 的
proxy_read_timeout默认可能只有几十秒,空闲连接会被断掉,需要调大,比如 3600s。 - 服务器或云厂商的 TCP keepalive 时间较短,需要在系统层调整
net.ipv4.tcp_keepalive_time。 - ttyd 底层 libwebsockets 会做协议层 ping/pong,但中间设备如果没有把心跳包转发过去,连接还是会悬空。
我的做法是在 Nginx 里把超时调到 3600 秒,同时按前面说的在 SSH 配置里加心跳。两者配合使用,日常基本不会再遇到"开个会回来终端挂了"的情况。
5.4 端口冲突与并发限制
如果部署时发现 7681 端口被占用,先查占用进程:
ss -lntp | grep 7681然后换一个端口,比如 8681。ttyd 的--max-clients参数可以限制并发 WebSocket 连接数,配置里写成:
--max-clients 32另外要注意,某些云服务器的安全组默认只放行 22 端口,7681 或自定义端口必须加到安全组规则里,否则服务即使起来了,外部也访问不到。这类问题通常表现为"本机能 curl,外网访问超时",排查时一看安全组基本就有答案。
6. 安全与合规提醒:WebSSH 暴露在公网等于裸奔
6.1 一定要做认证,不要裸奔
WebSSH 把 shell 搬进浏览器的同时,也把风险扩大到了浏览器。如果你直接把 ttyd 监听在 0.0.0.0:7681 且不带任何认证,那等于昭告全网:"来啊,我这里有一个不需要密码的 shell"。网络扫描器分分钟就能找到你。
所以最低限度的安全配置是:
- 设置
--login-credential,用强密码,不要用 admin/admin。 - 只监听
127.0.0.1,不要监听公网 IP。 - 通过 Nginx 反代,启用 HTTPS 和 Basic Auth 再加一层。
- 外层加 fail2ban 或 WAF 规则,对暴力尝试的 IP 做封禁。
6.2 我的三层防护方案
我现在生产环境跑 WebSSH 的防护组合是:
- 网络层:7681 端口只对本机开放,公网入口只保留 80/443 端口,由 Nginx 统一接管。
- 接入层:HTTPS 证书 + Nginx Basic Auth + ttyd 的
--login-credential双重认证。浏览器端要过两次密码验证,即使一次被泄露还有第二道。 - 行为层:使用独立低权限用户运行 ttyd,不要用 root 直接跑;配合 auditd 记录终端操作日志;必要时接上堡垒机审计系统,所有通过 WebSSH 执行过的命令都能回溯。
如果是在内网使用,防护等级可以适当降低,但"认证必须加、端口不要公网裸奔"这条红线不能破。一套配置上线前,建议先用扫描工具测一下外部视角能看到哪些端口,很多问题在扫描阶段就能暴露。
6.3 密钥管理的几个实操细节
WebSSH 本质上还是要用到 SSH 认证。如果让 ttyd 直接负责 SSH 到内网机器,宿主机上必然要放私钥,这个私钥的安全就等于内网的安全。我的习惯是:
- 使用专用的部署密钥,不要放个人日常密钥。
- 私钥权限设成
600,避免被同机其他用户读取。 - 不要把
StrictHostKeyChecking=no开给任意地址,尽量限定目标主机列表。 - 定期轮换密钥,并在前端页面提示使用者不要执行不明脚本。
命令审计上,如果条件允许,把 WebSSH 接入堡垒机或日志平台,每次登录、每个命令都留痕。小投入在出问题时能救命。比如有次排查线上配置变更,就是靠 WebSSH 的审计日志定位到是谁在什么时间执行了哪个命令,效率比挨个找人大得多。
最后分享一个我自己的小技巧:我会把 WebSSH 的访问地址做成书签,域名固定,路径带上用户名,每次连服务器就一个动作,省去找 IP、输端口、输密码的重复操作。遇到"手边没客户端"的尴尬现场,只要身边有任何一台能开浏览器的设备,WebSSH 就能把救急这件事稳下来。项目搞定了,需求也就学到了:运维工具未必都得装客户端,把能收敛到浏览器的能力收敛过去,反而更抗环境变化。