☰
Rocky9部署wetty:浏览器访问Web终端实战指南
2026/10/5 8:39:13 网站建设 项目流程

1. 为什么要在Rocky9上装Web终端:wetty的定位与适用场景

我先说个常见的尴尬场景:某天人在外面,笔记本没带,只有一台手机或者一台公用电脑,但生产环境的一台Rocky9服务器突然报错,需要马上登录上去看日志、重启服务。这个时候手边没有SSH客户端,Windows默认的PowerShell倒是能连,但公用电脑上你既不想装额外软件,也不能保证有权限装。更麻烦的是,如果这台服务器在内网,你还需要先跳板、再堡垒机,来回折腾。这时候,一个能直接在浏览器里打开、输入账号密码就能进Shell的Web终端,就非常实用了。

wetty就是这个定位的工具。它的全称是Web TTY,本质上是一个Node.js写的Web应用,把SSH客户端的功能封装成了一个网页界面。用户通过浏览器访问特定端口,就能得到一个交互式的终端会话,背后它帮你建立到目标主机的SSH连接。过程听起来简单,但实际搭建的时候,选什么系统、怎么配Node环境、怎么处理SSH密钥、怎么用Nginx做反向代理,这里面的坑并不少。

我为什么要专门讲Rocky9?因为Rocky Linux 9是一个很典型的现代企业级Linux发行版,默认仓库里带的Node.js版本、编译工具链、SELinux策略,都和以前的CentOS 7时代有显著差异。很多网上老的wetty教程还停留在CentOS 7、Node 8的年代,照搬到Rocky9上大概率会踩坑。这篇文章就是基于我在Rocky9上完整部署wetty的经验,把从环境准备、Node.js安装、wetty部署、SSH认证方式选择,到Nginx反向代理和SELinux处理的全过程做一个梳理。适合谁看?要么是需要在浏览器里快速管理服务器的运维同学,要么是搭建跳板机、给团队提供Web登录入口的DevOps工程师,也包括想折腾自建工具的个人开发者。

这里先给一个总体印象:wetty最新版本对Node.js版本有要求,Rocky9默认仓库里带的Node.js比较老,所以不建议直接dnf install nodejs就完事,推荐用NVM或者NodeSource的源装一个较新的LTS版本。另外,Rocky9默认开启SELinux,如果你打算用Nginx反向代理到wetty的端口,SELinux的httpd_can_network_connect这个布尔值不打开,Nginx会一直报502,这个坑我后面会详细说。

2. 环境准备:Node.js版本选择与npm镜像加速的关键细节

2.1 Rocky9默认Node.js版本的问题

Rocky Linux 9的AppStream仓库里提供了Node.js,但版本是16或者18,具体看仓库快照。听起来Node 18好像还能用?这里就要说清楚了:wetty从4.x版本开始,对Node.js的版本要求是18以上,建议20或者更高。如果你只是装一个最新版wetty跑起来,Node 18勉强能工作,但如果你后续要装一些依赖、跑npm install,可能会因为Node版本和某些依赖的engines字段冲突而报错。更稳妥的做法,是直接上Node.js 20 LTS,这也是wetty官方CI里主要测试的版本。

我自己第一次部署的时候,图省事直接dnf install nodejs,装完是Node 16(当时我用的Rocky9仓库还比较老),然后npm install -g wetty装完,一启动就报错:Error: Cannot find module 'node:stream/web'。这个错误就是Node版本太低导致的,wetty新版本用了Node内置模块,老版本Node根本不含这个模块。所以,环境准备阶段一定要把Node版本作为第一优先级。

2.2 用NVM安装Node.js 20 LTS

我推荐用NVM(Node Version Manager)来安装Node.js,原因有两个:第一,NVM装到用户目录,不需要动系统全局,后续升级、切换版本都很灵活;第二,NVM安装的Node二进制是官方预编译的,不会和Rocky9自带的GCC等工具链产生潜在的编译兼容性问题。

具体操作如下:

# 安装NVM,建议先确认最新版本号 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash # 让nvm命令在当前shell生效 source ~/.bashrc # 安装Node.js 20 LTS nvm install 20 # 设置默认版本 nvm alias default 20 # 验证 node -v npm -v

如果服务器在国内,有一步非常关键:npm install下载依赖包会非常慢,甚至超时失败。建议先把npm源切换成国内镜像,或者至少在安装wetty之前设置一下:

npm config set registry https://registry.npmmirror.com

这一步能帮你省下大量时间,尤其是wetty依赖的包不少,体积不小。

2.3 安装wetty本体

Node环境就绪后,安装wetty就一行命令:

npm install -g wetty

装完后,可以用which wetty确认安装位置,通常会在/root/.nvm/versions/node/v20.x.x/bin/wetty这种路径下,这跟系统自带Node的全局路径不一样,后面配置systemd服务的时候要用绝对路径,这点很容易被忽略。

看一下wetty的基本用法:

wetty --help

主要参数包括:

  • -p或--port:监听端口,默认3000
  • --host:监听地址,默认0.0.0.0
  • --ssh-host:SSH目标主机,默认127.0.0.1
  • --ssh-port:SSH端口,默认22
  • --ssh-user:SSH用户名,默认当前用户
  • --ssh-auth:认证方式,默认是auto,也可以用publickey或password
  • -k:SSH私钥路径

这里说明一下:wetty默认是"wan在浏览器里输入用户名密码,然后wetty帮你SSH登录"的模式。如果你只是想快速试一下,直接跑wetty,然后浏览器访问http://服务器IP:3000,输入本机的用户名密码就可以了。

但生产环境要考虑几个事情:

  1. wetty不能裸奔,至少要用密码或密钥保护wetty本身的web访问;
  2. SSH认证方式要结合实际环境选择;
  3. 常驻后台,需要一个systemd服务管理。

下面一步步展开。

提示:如果你只是想在内网快速开一个Web终端,第一步先别搞复杂,用默认模式跑通浏览器登录,再考虑反向代理、SSL、访问控制,这样排查问题更清晰。

3. 部署wetty的systemd服务:进程托管与启动参数调优

3.1 为什么要用systemd管理

直接在前台跑wetty,一关终端服务就没了,这在生产环境是不可接受的。用systemd管理的好处是:开机自启、崩溃自动重启、日志统一交给journald,排查问题非常方便。

要注意一个关键点:wetty的全局安装路径取决于NVM的安装路径。假设你的Node是通过NVM装的,那么:

which wetty # 输出类似 /root/.nvm/versions/node/v20.x.x/bin/wetty

在写systemd unit文件时,ExecStart必须写成完整的绝对路径,因为systemd环境下PATH可能不包含NVM的bin目录。

下面是完整的unit文件示例:

[Unit] Description=Web TTY - Wetty After=network.target [Service] Type=simple User=root # 这里一定要用绝对路径,并指定监听端口 ExecStart=/root/.nvm/versions/node/v20.x.x/bin/wetty --host 127.0.0.1 --port 3000 --ssh-host 127.0.0.1 --ssh-port 22 --ssh-auth password --title "Server Web Console" Restart=always RestartSec=3 [Install] WantedBy=multi-user.target

写好后:

# 写入unit文件 vim /etc/systemd/system/wetty.service # 重载配置 systemctl daemon-reload # 启动 systemctl start wetty # 设置开机自启 systemctl enable wetty # 查看状态 systemctl status wetty

3.2 监听127.0.0.1还是0.0.0.0

很多人一开始图方便,直接把wetty的--host设为0.0.0.0,浏览器访问http://IP:3000就直接能用了。这样做确实快,但问题也明显:

  • 3000端口直接暴漏公网,wetty本身是一个登录入口,暴力破解风险一直存在;
  • 没有TLS加密,账号密码走明文HTTP传递,在公网上这是不可接受的;
  • wetty的Web页面没有内置的多用户登录逻辑,它拿到的是你系统里的账号密码,安全边界比较模糊。

所以更稳妥的方式是:wetty只监听127.0.0.1:3000,前面再加一层Nginx做TLS终结和访问控制。这样在浏览器和Nginx之间走HTTPS,Nginx和wetty之间走本地HTTP,再加上如果wetty本身绑了本地地址,外部网络无法直接扫描到3000端口,安全提升非常明显。

3.3 SSH认证方式的选择:password、publickey、auto

wetty支持三种SSH认证方式,这个选择直接影响使用体验和安全:

认证方式说明适合场景
password用户在Web页面输入目标主机的SSH密码,wetty用这个密码做SSH登录目标主机只有密码认证、没有密钥的场景
publickeywetty使用预先配置的SSH私钥免密登录目标主机,Web端无需输入密码管理多台服务器、密钥统一管理的场景
auto自动判断,优先私钥,无法用私钥时回退密码登录行为不确定的场景

说说我的经验:

  • 如果wetty和目标主机是同一台机器,也就是通过wetty登录本机的Shell,那么password模式就够用,用户输入的是Linux系统账号密码。
  • 如果wetty作为跳板机,要通过它登录多台内网服务器,那我强烈建议用publickey模式,在wetty所在机器上为不同角色准备好SSH私钥。这样Web端可以不用泄露密码,SSH层完全走密钥认证,更安全。
  • 用publickey模式时,启动参数要加--ssh-auth publickey --ssh-key /path/to/id_rsa,还要注意私钥文件权限必须是600,否则SSH会直接拒绝加载。

从实际运维角度讲,我更推荐publickey模式,因为密码认证意味着要把服务器密码暴露给使用Web终端的人,分工一多,密码管理就混乱了。用密钥一次性配置好,收回来也方便,改一下文件权限和Nginx访问控制就能完成权限回收。

3.4 启动参数细节与常见异常

启动wetty时,有几个参数是我实际踩过坑的:

关于--ssh-key:如果密钥文件路径写错,wetty不会立即报错,而是在浏览器登录的那一步卡住,表现是一直转圈然后超时。排查时看journalctl -u wetty -f的日志,会看到类似Permission denied (publickey)的记录,只有到这一步才能定位到是密钥加载失败还是远程主机拒绝了密钥。

关于--ssh-known-hosts:SSH连接目标主机时,有StrictHostKeyChecking的问题。首次连接会询问是否信任主机指纹,但wetty是一个Web服务,你没法在浏览器里回答这个问题。因此需要把目标主机的指纹预先写入known_hosts文件。操作方式:

# 以root用户执行,手动SSH一次要登录的目标主机 ssh-keyscan -H 192.168.1.100 >> /root/.ssh/known_hosts

如果你不处理这个文件,wetty在公钥模式下连接新主机时就会卡住。很多人在这里找不到原因,实际上是known_hosts没准备好。

关于--title:这个参数会给浏览器标签页设置标题,建议配置一个明确名称,比如Production Server Web Console,方便你同时开了多个Web终端时区分。

3.5 测试与验证

服务配置完成后,验证步骤建议这样走:

  1. 先确认wetty进程在跑:systemctl status wetty
  2. 确认端口监听:ss -tlnp | grep 3000,应该看到127.0.0.1:3000
  3. 本地测试:在服务器上直接curl -I http://127.0.0.1:3000,应该返回HTTP 200或者301
  4. 浏览器访问:如果是本机先测试,直接http://127.0.0.1:3000能看到登录页面

这里特别提醒一点:wetty的Web页面在外观上没有任何"正在连接"的提示,它看起来就像一个终端黑屏。当你输入用户名密码或直接看到Shell提示符时,说明连接已经建立。如果页面一直黑屏,多半是SSH认证失败了,优先检查journal日志。

4. Nginx反向代理与TLS配置:把wetty安全地暴露出去

4.1 为什么必须加一层Nginx

我在生产环境推荐的结构是这样的:

浏览器 --> Nginx(443, TLS) --> wetty(127.0.0.1:3000) --> SSH(目标主机)

这么做的理由很实际:

  1. 只需要暴露443端口,其他端口一律关闭,减少暴露面;
  2. 用Let's Encrypt证书免费做TLS,账号密码不再明文传输;
  3. 可以在Nginx层加auth_basic,给wetty的Web端口再加一道访问凭证,适合只给特定人员使用;
  4. Nginx可以配置访问日志、限流,后续审计更方便。

如果你直接用systemctl stop firewalld这种图上省事的方式,把3000端口暴露出去,那我只能说希望你的服务器不是公网IP。安全措施能加一层就加一层,别偷懒。

4.2 Nginx安装与基础配置

dnf install -y nginx systemctl enable --now nginx

默认站点配置先不碰,直接在/etc/nginx/conf.d/下新建一个专门的反向代理配置:

server { listen 443 ssl http2; server_name your-domain.example.com; ssl_certificate /etc/letsencrypt/live/your-domain.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/your-domain.example.com/privkey.pem; # 基础安全响应头 add_header X-Frame-Options SAMEORIGIN always; add_header X-Content-Type-Options nosniff always; location / { proxy_pass http://127.0.0.1:3000; 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; } } # HTTP自动跳转HTTPS server { listen 80; server_name your-domain.example.com; return 301 https://$host$request_uri; }

这里面最关键的是后面四行proxy_set_header。wetty用于Web终端,依赖WebSocket进行双向通信,所以必须正确设置Upgrade和Connection头,否则浏览器能打开页面,但终端无法建立WebSocket连接,表现就是页面一直空白、加载不出来。

4.3 Nginx配置后502错误:SELinux是头号嫌疑

这是我在Rocky9上遇到最多的问题。明明Nginx配置没问题,wetty进程也在跑,curlhttp://127.0.0.1:3000也正常,但通过Nginx访问就是502 Bad Gateway。查Nginx错误日志,看到的是connect() failed (13: Permission denied)。

这个报错十有八九是SELinux拦截了。Rocky9默认强制开启SELinux,Nginx的SELinux策略默认不允许它作为代理去连接后端网络的端口,除非你显式打开httpd_can_network_connect这个布尔值。

解决办法:

# 打开Nginx到网络连接的后端连通性 setsebool -P httpd_can_network_connect 1

-P参数表示持久化,重启后依然生效。改完后,最好验证一下:

getsebool httpd_can_network_connect # 输出 httpd_can_network_connect --> on

这里我多说一句:网上很多教程一遇到SELinux相关报错就直接让你setenforce 0,这是非常不负责任的做法。setenforce 0只是临时关闭,重启后SELinux又会开启,而且等于放弃了Rocky9的安全增强机制。正确做法是针对具体需求放开具体的布尔值或端口上下文,安全性和可用性兼顾。

4.4 处理WebSocket超时与长连接

Web终端和普通HTTP请求不一样,它会保持一个长连接。如果中间有Nginx代理,你要防止Nginx或者网络设备因为超时把连接掐断。

在Nginx的location /里,建议加两个参数:

proxy_read_timeout 3600s; proxy_send_timeout 3600s;

默认值是60秒,如果不改,你浏览器里的终端挂起超过60秒没有操作,连接就会被Nginx断开。虽然断开后wetty可能重新连接,但体验非常差。

如果wetty和用户之间的网络本身不稳定,还可以在系统层面启用TCP保活,但大多数场景下Nginx的超时配置就够了。

4.5 Nginx Basic Auth加固

如果你不想让所有知道域名的人都能打开登录页面,可以在Nginx层加一层HTTP Basic Auth:

# 创建认证文件,用户名例如admin printf "admin:$(openssl passwd -apr1 你的密码)" > /etc/nginx/.wetty_auth

然后在location /内增加:

auth_basic "Restricted"; auth_basic_user_file /etc/nginx/.wetty_auth;

这样浏览器弹出一个账号密码框,通过了才进入wetty页面。这一道凭证和SSH登录凭证是两个层面,等于在Web入口前再加一道门,安全防护明显更好。

不过要注意:如果你的用户需要从不同设备登录,Basic Auth的凭证是明文缓存在浏览器里的,如果设备丢失,需要及时修改认证文件。另外,Basic Auth不是替代TLS的,不要因为加了Basic Auth就放弃HTTPS,密码一样会被抓包。

5. 常见问题排查与避坑实录

5.1 页面能打开但终端一直黑屏

这个现象我见过很多次,可能性按概率排序:

  1. WebSocket代理未配置:Nginx没加Upgrade头,浏览器的WebSocket握手直接失败。浏览器F12打开控制台,如果看到WebSocket connection failed,基本就是这个问题。
  2. SSH认证失败:特别是公钥模式下,密钥路径错误、权限不对、known_hosts缺失,都会导致wetty后台SSH连不上。看wetty的journal日志能发现端倪。
  3. wetty监听地址和代理目标不一致:比如wetty监听的是0.0.0.0,Nginx代理127.0.0.1:3000自然没问题;但如果wetty监听只绑了127.0.0.1,外部Nginx代理127.0.0.1:3000反而正常,一旦你绑了::1这类IPv6地址,代理就会失败。

排查时,优先看日志:

journalctl -u wetty -f journalctl -u nginx -f

一般几分钟内就能定位问题。

5.2 浏览器直接访问3000端口不安全

我在第3节说过,wetty监听127.0.0.1是推荐方案。但有一种情况要注意:如果你在服务器上用Docker或者K8s部署wetty,Nginx可能不在同一个网络命名空间,代理127.0.0.1就代不过去了。这时候要么让wetty监听容器网络的IP,要么用--host 0.0.0.0但加防火墙规则,把3000端口只放行给特定的来源IP。

在Rocky9上,用firewalld限制非常方便:

firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port port="3000" protocol="tcp" accept' firewall-cmd --reload

这样只有内网网段能访问3000端口,公网依然不行。如果你直接用Nginx反代,其实3000端口根本不需要对外部开放,只开放Nginx的80/443即可。

5.3node:stream/web模块找不到

这个在第2节提到过,Node版本过低导致的。Node 16及以下不具备node:stream/web模块,wetty新版启动即报错。解法也很简单:切到Node 20 LTS,然后用NVM管理,别用系统自带老版本。

补充一个容易误判的细节:如果你用NVM切完Node版本,重新npm install -g wetty,此时wetty的安装路径会跟着变。原来旧版本的wetty还残留在旧Node的bin目录下,所以你which wetty看到的可能是新路径,但systemd服务如果写死的是旧路径,还是会启动旧版本。所以切换Node版本后,务必同步更新systemd的ExecStart路径。

5.4 用ssh-keyscan预配置known_hosts

在公钥模式下,如果wetty连接的主机是首次连接,SSH会有指纹确认提示,而wetty无法处理这种交互,导致连接失败。解决办法是在wetty所在机器上,提前把目标主机指纹抓进known_hosts:

ssh-keyscan -H 192.168.1.100 >> /root/.ssh/known_hosts

这里建议用root执行,因为wetty服务在systemd下默认以root运行,读取的known_hosts是/root/.ssh/known_hosts,不是普通用户的。很多人在这里搞混,明明已经ssh-keyscan了,但wetty还是失败,就是因为写入到了普通用户的家目录,而wetty读取的是root的家目录。

不过要注意:如果你用User=root运行wetty服务,私钥路径和known_hosts都要放在/root/.ssh/下。如果你为了安全,把wetty改为nobody或其他用户运行,那对应的路径就要改成那个用户的目录,文件权限也要相应处理。

5.5 SSH密码认证被暴力尝试

公网环境,wetty如果放在公网入口,即使是通过Nginx反代,只要有人知道路径,就会尝试破解SSH密码。我的建议是:

  1. 生产环境优先使用公钥认证,关闭目标主机的密码登录;
  2. 在Nginx层加Basic Auth或IP白名单;
  3. 如果不能关密码认证,建议用fail2ban监控SSH日志,自动封禁可疑IP。

fail2ban在Rocky9上安装并不复杂,但要注意其默认的SSH jail通常监控的是系统真正的SSH服务,而wetty发起的SSH连接同样是走系统SSH,所以fail2ban对这种经由web入口的暴力破解同样有效。只不过fail2ban封的是来源IP,如果攻击者用大量代理IP,作用有限,所以还得靠公钥认证和Basic Auth做兜底。

6. 进阶用法:wetty多主机访问与登录用户隔离

6.1 通过不同端口映射不同主机

wetty本身一次启动只连接一个SSH目标,但你可以同时启动多个wetty实例,监听不同端口,通过Nginx将不同路径转发到对应实例。例如:

https://your-domain.example.com/web/alpha --> wetty1 (127.0.0.1:3101) --> 192.168.1.10 https://your-domain.example.com/web/beta --> wetty2 (127.0.0.1:3102) --> 192.168.1.20

Nginx配置类似:

location /web/alpha { proxy_pass http://127.0.0.1:3101; # 其他代理参数同理 }

不过要留意,wetty的WebSocket路径默认是在根路径/上的,如果你用子路径代理,可能需要wetty的--base参数或者额外改写,否则WebSocket连接会失败。我的经验是,为了减少麻烦,直接用不同子域名或者同一域名的不同端口代理到不同wetty实例更省事,用子路径需要确认wetty是否支持base path,不同版本支持情况不一样。

6.2 多用户隔离与权限控制

wetty本身是一个终端窗口,登录后执行的是系统账号的Shell,没有内置"允许A用户只执行某些命令"这种细粒度控制。如果你需要对用户做命令级限制,有两条路:

  1. 在Linux系统层面,为不同用户设置受限的Shell或使用rbash(受限bash);
  2. 借助SSH层的authorized_keys中的command=属性,限制某个公钥登录后只能执行特定命令。

第二种方式更好,因为它是SSH层的能力,wetty完全不需要关心。在目标主机的~/.ssh/authorized_keys里写成:

command="/usr/bin/tail -f /var/log/nginx/access.log" ssh-rsa AAAA... comment

这样即使通过wetty登录,拿到的也只是一个tail命令的输出,而不是Shell。

6.3 与Jump Server结合

如果你有过堡垒机、跳板机的场景,wetty可以作为跳板机上的Web访问入口,再配合ssh -J的ProxyJump能力,让wetty先连接跳板机,再跳转到目标内网机器。不过这会把拓扑搞复杂,我建议还是保持一个wetty指向一个目标主机的简单映射,多主机就用多个实例。运维的核心是稳定可预期,过度设计反而容易出问题。

6.4 日志审计思路

因为wetty的所有SSH操作都落在系统SSH日志里,你可以:

  • 配置SSH的LogLevel VERBOSE,记录详细的会话信息;
  • 通过journalctl -u wetty查看Web层面的连接日志;
  • 在Nginx的access_log里记录来源IP、时间、请求路径。

如果合规要求更高,可能需要开启内核审计auditd,或者接一个专门的日志采集系统。wetty本身不记录终端里输入的命令内容,这一点要有心理预期,别指望它替代专业的运维审计系统。

7. 写在最后:我的实际建议与注意事项

部署wetty这件事,说难不算难,但如果不了解Rocky9和它之间的几个关键结合点,真能折腾半天。我再把最重要的经验浓缩一下:

第一,Node.js版本一定要用20 LTS,别用系统自带的老版本,这是最省心的方案。装Node用NVM,管理方便,升级容易。

第二,wetty别裸奔,前面一定加Nginx和TLS。监听地址用127.0.0.1,外部只开放443。这不是制造麻烦,而是给你自己减少后顾之忧。

第三,SSH认证能走公钥就走公钥。密码认证虽然简单,但安全性差,运维管理也混乱。公钥一旦配好,后续新增机器、权限回收都清晰。

第四,Rocky9上的SELinux不是摆设,setsebool -P httpd_can_network_connect 1要记住,遇到502先想到它。

最后一件事:部署完成后,记得测试一下重启后wetty是否自动启动了。我的习惯是跑一遍完整的验证流程:重启机器、检查wetty服务状态、浏览器访问登录页面、执行一条命令,确认无碍。这样才算真正把服务交付出去了。如果你照着这篇文章搭完,遇到和我不同的报错,建议先看日志,再逐步检查Nginx代理、WebSocket头、SELinux、SSH认证这几个环节,大概率能定位到问题所在。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询