ttyd 网页终端实战:浏览器里的 SSH、tmux 会话共享、Nginx WebSocket 反代与安全加固全流程

什么时候你会需要「浏览器里的终端」

对个人站长来说,总有那么几个场景:你在同事的电脑上、在只带了一个浏览器的公共机器上、或者干脆在手机上,突然需要登进服务器看一眼日志、重启一个服务、改一行配置。装 SSH 客户端不方便,网页控制台又是云厂商那种卡顿的 VNC。ttyd 解决的正是这个问题——它把任意命令行程序(本质上是 bash / tmux)通过 WebSocket 暴露成一个网页终端,你打开浏览器就能得到一个完整、流畅、响应迅速的 shell。

ttyd 用 C 写成,单二进制、依赖极少(只需要 libwebsockets 和 libuv),内存占用比 Node 系的 web 终端(如 wetty)低一个数量级,几十兆之内就能跑起来。它支持中文、支持复制粘贴、支持多客户端共享同一会话(配合 tmux 时非常有用),也支持只读模式。这篇文章从部署讲到 Nginx 反代,再到最重要的安全加固——因为把一个 shell 挂到公网上,安全做不好就是给服务器开后门。

最简部署:先在本机 127.0.0.1 上跑起来

永远不要一上来就 -p 80 直接对外。正确的顺序是:先绑定到本机回环地址验证,再通过反向代理暴露,最后加认证。安装:

# Debian/Ubuntu 仓库里的版本(够用,但可能偏旧)
sudo apt-get update && sudo apt-get install -y ttyd

# 或用官方最新静态二进制
curl -fsSL -o /usr/local/bin/ttyd \
  https://github.com/tsl0922/ttyd/releases/latest/download/ttyd.x86_64
chmod +x /usr/local/bin/ttyd

ttyd --version

先做一个「只读、只绑定本机」的冒烟测试,确认二进制能跑、网页能开:

# 绑定 127.0.0.1:7681,只允许看,不给交互
ttyd -p 7681 -i lo -R bash

# 另开一个终端验证端口
ss -ltnp | grep 7681
curl -sI http://127.0.0.1:7681/ | head -1

这时候用 SSH 端口转发在自己电脑上看一眼效果:ssh -L 7681:127.0.0.1:7681 you@server,然后浏览器开 http://localhost:7681。确认界面正常、中文不乱码、能输入命令(-R 会让它变只读,去掉即可交互),再进入下一步。

用 systemd 常驻,并通过开关控制启动

不要让 ttyd 裸跑在 shell 里,用 systemd 管起来。关键点:绑定到回环、指定工作目录、限制资源、开启只读默认。真正对外的那一层交给 Nginx,ttyd 自己只监听 127.0.0.1。

# /etc/systemd/system/ttyd.service
[Unit]
Description=ttyd web terminal
After=network.target

[Service]
Type=simple
# 只绑定回环;用 tmux 让多客户端共享/断线不丢会话
ExecStart=/usr/local/bin/ttyd \
  --interface 127.0.0.1 \
  --port 7681 \
  --writable \
  --max-clients 3 \
  tmux new -A -s webterm
Restart=on-failure
RestartSec=3
# 降权运行:专用低权限用户,绝不用 root
User=webterm
Group=webterm
WorkingDirectory=/home/webterm
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

注意 tmux new -A -s webterm 这个命令:-A 表示「会话已存在就 attach,不存在就新建」,-s webterm 是会话名。这样一来,你从浏览器连上去、平板再连一次,看到的是同一个 tmux 会话,而不是两个互相独立的 shell;网络断了重连,之前跑的命令和输出还在。这是网页终端体验好坏的分水岭。

# 创建专用低权限用户
sudo useradd -r -m -s /bin/bash webterm

sudo systemctl daemon-reload
sudo systemctl enable --now ttyd
sudo systemctl status ttyd --no-pager

特别提醒:--writable 给的是交互式 shell,不是只读预览。这意味着任何能连上的人都能以 webterm 用户的身份执行命令。所以下面的认证和网络层加固不是可选项。

Nginx 反代:路径、WebSocket 升级与超时

ttyd 内部用的是 WebSocket,Nginx 反代时最容易踩的坑是忘了传递 Upgrade 头,结果是页面能打开但一片空白、连不上终端。完整配置如下:

# /etc/nginx/conf.d/ttyd.conf
map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl http2;
    server_name term.example.com;

    ssl_certificate     /etc/letsencrypt/live/term.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/term.example.com/privkey.pem;

    # 强烈建议:整个站点只允许特定 IP 访问
    allow 203.0.113.10;      # 你的固定出口 IP
    allow 198.51.100.0/24;   # 办公室网段
    deny  all;

    location / {
        proxy_pass http://127.0.0.1:7681;
        proxy_http_version 1.1;

        # 这两行是 WebSocket 的关键,缺了就连不上
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $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;

        # 终端长连接不能被 Nginx 掐断
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
        proxy_buffering off;
    }
}

四个要点逐一说清楚:

  • map $http_upgrade $connection_upgrade:把它放在 http {} 块里(放在 server{} 里会语法报错)。它负责在客户端发起 WebSocket 握手时把 Connection: upgrade 透传给后端。
  • proxy_http_version 1.1:WebSocket 升级依赖 HTTP/1.1,默认的 1.0 不支持,必须显式写。
  • proxy_read_timeout:默认 60 秒,终端一旦 60 秒没数据就会被断开。改成 3600s 让长连接稳定存活。
  • proxy_buffering off:终端输出要实时到达浏览器,开启缓冲会造成明显延迟。终端和普通网站不同,它的响应是对「输入」逐字流式回显的,任何一层缓冲都会把这种即时感破坏掉——你按下回车后命令输出要停顿一两秒才出现,用起来非常难受,所以这个开关在网页终端场景里必须关。

还有一个容易被忽略的细节是 proxy_set_header Host $host; 这一行。ttyd 内部在生成 WebSocket 握手地址时会参考 Host 头,如果反代时 Host 被改成了 127.0.0.1,某些版本的前端就会把 WebSocket 连到错误的目标上,表现为页面打开、界面正常、但终端永远「正在连接」。保留原始 Host 能避开这个隐蔽问题。

配好之后 nginx -t && systemctl reload nginx,从允许的 IP 打开 https://term.example.com 验证。allow/deny 是这里第一道、也是最有效的一道防线——把攻击面从「全世界」直接缩小到「你的几个 IP」。

认证加固:三层防护,缺一不可

把 shell 挂上公网,认证是生死线。推荐叠三层,任何一层被绕过都还有下一层兜底。

第一层:Nginx 基础认证(Basic Auth)。

sudo apt-get install -y apache2-utils
# 用户名 webadmin,密码交互式输入(不要写进命令历史)
sudo htpasswd -c /etc/nginx/.htpasswd webadmin

# 在 location / 里加上:
auth_basic           "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;

Basic Auth 的密码是 base64 而非加密传输,所以必须配合 HTTPS(上面已经配了)。它的意义在于:即使有人猜到你的 URL,没有密码也连不上 WebSocket。

第二层:ttyd 自带凭据。ttyd 支持 --credential user:pass,让后端自己也校验一次。这样即便 Nginx 配置被改动、认证被摘掉,ttyd 仍然拒绝未授权连接:

# 在 ExecStart 里加(注意:密码会出现在 systemd unit 中,注意文件权限)
--credential webadmin:一个足够长的随机密码

第三层:只读降级。若你的场景只是「看日志」,就不要开 --writable,用 -R 只读模式。只读模式下即便被攻破,攻击者也只能看,不能执行命令。很多人给 ttyd 开通写权限只是为了查日志,这是没必要的暴露。

此外还有两个容易忽略的点:(1)低权限用户 webterm 不应在 sudoers 里,并且它的 shell 环境要尽量干净,不要 source 到含有敏感 token 的 rc;(2)ttyd 能访问的文件取决于 webterm 用户的权限,所以它天然看不到只有 root 能读的 /etc/shadow、网站私钥等——这个权限隔离本身就是一层保护,别图省事用 root 跑。

安全审计:开完 ttyd 一定要做的几件事

部署完成后,做一次自查,把每一项都落实:

  • 端口暴露检查:ss -ltnp | grep 7681 应显示 127.0.0.1:7681,绝不能是 0.0.0.0:7681。是后者说明 --interface 没生效或被防火墙之外的路径暴露了。
  • 防火墙:用 ufw 或 nftables 只放行 443,7681 应对公网完全关闭。sudo ufw status verbose 确认。
  • 访问日志:给 location 单独配日志,access_log /var/log/nginx/ttyd.access.log;,一旦有异常连接能追到 IP。
  • 失败封锁:给这个 server 块加 fail2ban 规则,连续多次 401 就封 IP,抵御撞库。
  • 会话隔离:给 ttyd 加 --max-clients 2,避免有人疯狂开连接耗尽资源。
  • 审计更危险的操作:如果 webterm 有 sudo 权限,配合 auditd 记录其执行过的命令。
# 快速验证清单
ss -ltnp | grep 7681                 # 必须是 127.0.0.1
sudo ufw status verbose              # 7681 应无公网规则
curl -sI https://term.example.com | head -1         # 应 401(未认证)
curl -s -o /dev/null -w '%{http_code}\n' \
  -u webadmin:wrongpw https://term.example.com      # 应 401

和其他方案的取舍:ttyd vs wetty vs SSH 隧道 vs 云控制台

要不要上 ttyd,取决于你的约束。wetty 基于 Node.js,配置更复杂、内存占用更高,没有明显优势;纯 SSH 隧道(ssh -L)不需要在服务器上开任何额外端口,安全性最高,但前提是你能用 SSH 客户端,手机上体验较差;云厂商控制台(VNC/串口)不依赖系统里的任何服务,是最后的救命稻草,但卡顿且复制粘贴困难。ttyd 的独特价值在于「只要有浏览器就能用、并且能做得相对安全」——尤其适合手机上临时看一眼日志、或面对一台无法装 SSH 客户端的机器。

要强调的是,ttyd 不能替代 SSH。它是「临时 / 应急 / 移动端」通道,日常运维仍然应该走 SSH + 密钥。安全地使用 ttyd 的公式是:绑定回环 + Nginx TLS + IP 白名单 + 双层认证 + 低权限用户 + 只读优先,六项齐全才推荐对外。少任何一项,都是在公网上给服务器留一个可能通向 root 的入口。

小结

ttyd 用极小的体积(单二进制、几十兆内存)把服务器终端搬进了浏览器,配合 tmux 还能做到「多端共享、断线不丢会话」。部署只需三步——本机冒烟、systemd 常驻、Nginx 反代——但真正决定成败的是安全加固:WebSocket 反代的两个头、read_timeout 的调整、认证三层叠加、以及最重要的「永远绑定回环、永远别用 root」。把这套流程走通,你就在不牺牲安全的前提下,给自己多了一个随手可用的运维入口。

Last modification:October 15th, 2026 at 02:12 pm

Leave a Comment