
搞运维这些年我遇到过最尴尬的场景之一就是人在现场或临时换了台电脑手头却没有装任何 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 听起来是个小工具生态里其实有不少实现。我自己试过或评估过的主要有四类方案语言/技术栈特点部署难度ttydC轻量、无 Node 依赖、性能好低WettyNode.js纯 npm 安装配置直观低sshwiftyGo功能全自带用户管理和多连接管理中webssh2Node.js支持多连接、文件上传下载中我第一次实际用的是 Wettynpm 一条命令就能装上手非常快。但用了半年后遇到两个问题一是 Node 版本兼容要求比较苛刻老服务器上装新 Node 反而麻烦二是并发连接一多内存占用会往上跳。后来切换到 ttyd整体感受明显改善我也一直用到现在。2.2 为什么我把 ttyd 当主力选 ttyd 有这几个原因轻。编译完只有一个二进制文件没有运行时依赖拷到任何 Linux 服务器上就能跑。这个特性在客户环境里特别省事不动系统、不改全局软件源。稳。ttyd 底层走 libwebsocketsWebSocket 的稳定性比我在 Wetty 上遇到的情况好不少长时间挂机不容易断。快。启动速度快、页面加载速度快弱网环境下表现也还行。参数直接。支持-p、-i、--login-credential、--ssl这些命令行参数写 systemd 服务时非常清晰。还有一个关键点ttyd 启动时可以直接指定要执行的命令不一定是开一个系统登录 shell。比如我实例里让它执行ssh -i /path/key user10.0.0.10浏览器登录后直接就进了内网机器使用者完全不需要关心内网 IP 和密钥在哪。这种网页打开即登入的体验用来做临时访问和轻度协作非常舒服。2.3 为什么不建议花精力自研有段时间团队里考虑过要不要用 xterm.js WebSocket 自己写一套。后来评估下来没有继续原因很实在终端这东西看着简单真正项目里会有一堆边角比如终端尺寸自适应、编码转换、特殊按键模拟、复制粘贴安全策略、多标签管理、审计日志对接每一项都要时间打磨。ttyd 这类成熟方案已经把这些问题处理得差不多了直接在它上面包一层鉴权和审计性价比高得多。除非你是那种有特殊交互需求的大团队否则我建议直接复用开源方案把精力花在怎么用好、怎么管好上。3. 实操把 ttyd 部署到服务器3.1 编译安装还是 Docker如果你只是在自己可控的服务器上用我强烈建议直接用 Dockerdocker 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] Descriptionttyd SSH web client Afternetwork.target [Service] ExecStart/usr/local/bin/ttyd -p 7681 -i 127.0.0.1 --login-credential ops:webpass login Restartalways RestartSec5s Userops [Install] WantedBymulti-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 StrictHostKeyCheckingno ops10.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 LANGen_US.UTF-8 login注意我写的是env LANGen_US.UTF-8 login这比在 shell 里 export 更直接能保证登录后的环境变量正确。退格键行为不对通常是终端类型问题。ttyd 前端用的是 xterm.js按理说应该支持正常退格。如果按退格变成^H检查你的 shell 设置了正确的TERMxterm-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避免被同机其他用户读取。不要把StrictHostKeyCheckingno开给任意地址尽量限定目标主机列表。定期轮换密钥并在前端页面提示使用者不要执行不明脚本。命令审计上如果条件允许把 WebSSH 接入堡垒机或日志平台每次登录、每个命令都留痕。小投入在出问题时能救命。比如有次排查线上配置变更就是靠 WebSSH 的审计日志定位到是谁在什么时间执行了哪个命令效率比挨个找人大得多。最后分享一个我自己的小技巧我会把 WebSSH 的访问地址做成书签域名固定路径带上用户名每次连服务器就一个动作省去找 IP、输端口、输密码的重复操作。遇到手边没客户端的尴尬现场只要身边有任何一台能开浏览器的设备WebSSH 就能把救急这件事稳下来。项目搞定了需求也就学到了运维工具未必都得装客户端把能收敛到浏览器的能力收敛过去反而更抗环境变化。