tmux-next 本身没有任何认证,只绑在 127.0.0.1 上。想从手机或外网访问,
需要在前面放一个反向代理来做两件事:HTTPS,和登录认证。这份指引用
Caddy——它能自动申请和续期证书,配置也短。
先想清楚风险。能访问到这个服务的人,就能在你的机器上执行命令—— 它给的是 shell。把访问权限当成 SSH 访问来对待:认证必须有,密码要强,只走 HTTPS。
tmux.example.com(Caddy 会为它自动签发证书)。127.0.0.1:7682(默认端口)。一个是登录密码的哈希,一个是给 WebSocket 用的随机口令(下一步解释为什么需要它):
# 密码哈希——把它填进 Caddyfile,别存明文密码 caddy hash-password --plaintext '你的强密码' # WebSocket 用的随机口令——生成一个,记下来 openssl rand -hex 24
第一条会输出一串 $2a$… 的 bcrypt 哈希;第二条会输出一串随机十六进制。
两个待会都要用。
下面是一份完整可用的配置。把三处占位符换成你自己的值:
tmux.example.com {
# WebSocket:单独用 cookie 认证,原因见下一步
@ws path /ws
handle @ws {
@noauth not header Cookie *tn_auth=你的随机口令*
respond @noauth 403
reverse_proxy 127.0.0.1:7682
}
# 其余所有请求:Basic Auth,通过后种下 cookie
handle {
basic_auth {
你的用户名 $2a$…你的密码哈希
}
header +Set-Cookie "tn_auth=你的随机口令; Path=/; Secure; HttpOnly; SameSite=Strict"
reverse_proxy 127.0.0.1:7682
}
}
三处占位符:tmux.example.com 换成你的域名;
你的随机口令 两处都换成第 2 步 openssl 生成的那串(必须一致);
你的用户名 和 $2a$… 换成登录名和第 2 步的密码哈希。
这是最容易踩坑、也最容易留下安全漏洞的地方。终端的实时数据走 WebSocket
(路径 /ws),而浏览器发起 WebSocket 握手时不会带 Basic Auth 头。
结果就是:如果你只用 Basic Auth 保护整站,页面看起来锁住了,
但 /ws 会因为收不到认证而连不上——或者更糟,你为了让它能连而放开了 /ws,
于是终端对任何人敞开。
上面的配置绕开了这个问题:普通请求通过 Basic Auth 后,响应里种一个 cookie;
之后浏览器发起 WebSocket 握手时会带上这个 cookie,
/ws 那段就凭 cookie 放行。所以那串随机口令必须保密——
它等价于一把钥匙,Secure 和 HttpOnly 标记确保它只走 HTTPS、
也不被脚本读到。
caddy reload --config /path/to/Caddyfile
然后从命令行验证三个关键点(把域名换成你的):
# 页面要求认证 → 期望 401 curl -so /dev/null -w "%{http_code}\n" https://tmux.example.com/ # WebSocket 没带 cookie → 期望 403 curl -so /dev/null -w "%{http_code}\n" https://tmux.example.com/ws # 带上正确 cookie → 期望 400(说明已穿过认证、到达服务) curl -so /dev/null -w "%{http_code}\n" \ -H "Cookie: tn_auth=你的随机口令" https://tmux.example.com/ws
401 和 403 证明两道门都锁着;带 cookie 的请求返回 400
是正常的——那是服务在说「这不是一个合法的 WebSocket 握手」,但请求确实到了它,
说明认证放行了。三个都对,就可以在手机上打开域名登录了。
换机器 / 换域名?这份配置不依赖任何特定系统。
只要 tmux-next 在某处监听 127.0.0.1:7682、Caddy 能解析到你的域名,
照抄即可。开机自启(launchd / systemd)等更多细节见仓库的
deploy.md。
上面的 Basic Auth 有两个已知的粗糙之处:登录框是浏览器画的原生弹窗, 没法自定义;那串 cookie 口令是静态的、永不过期,一旦泄漏就得手动改配置。 如果想要一个像样的登录页,加上会自动过期的 token,可以用 caddy-security 插件—— 它一并解决这两点。
它不在标准 Caddy 里,需要用 xcaddy 构建一个带它的版本:
xcaddy build --with github.com/greenpau/caddy-security
然后配置分三块。先生成一个签名密钥(JWT 用它签发和校验,泄漏等于伪造登录):
openssl rand -hex 32
① 全局块——定义用户库、登录门户、授权策略:
{
# 这两行让插件的中间件排到正确的位置(照抄,与站点用不用 basicauth 无关)
order authenticate before respond
order authorize before basicauth
security {
# 用户库:密码用 bcrypt 哈希,不存明文
local identity store localdb {
realm local
path /path/to/users.json
user 你的用户名 {
name 你的名字
email you@example.com
password "bcrypt:10:$2a$10$…你的密码哈希"
roles authp/user
}
}
# 登录门户:签发会过期的 JWT
authentication portal myportal {
crypto default token lifetime 28800 # token 有效期,秒(这里 8 小时)
crypto key sign-verify 你的签名密钥
enable identity store localdb
# 让 JWT cookie 在 auth. 和 tmux. 两个子域间共享
cookie domain example.com
}
# 授权策略:校验 JWT,未登录就跳到登录门户
authorization policy tmuxpolicy {
# 跨子域必须写绝对 URL,指向登录门户
set auth url https://auth.example.com/auth/
crypto key verify 你的签名密钥
allow roles authp/user
}
}
}
② 登录门户的站点块——它托管那个自定义登录页:
auth.example.com {
route /auth* {
authenticate with myportal
}
}
③ tmux-next 的站点块——用策略保护,未登录自动跳去登录页:
tmux.example.com {
# 未登录访问任何路径(含 WebSocket)都要求先有合法 JWT
authorize with tmuxpolicy
reverse_proxy 127.0.0.1:7682
}
为什么这次 WebSocket 不用单独处理了?因为 JWT 存在 cookie 里,
浏览器发起 WebSocket 握手时会带上 cookie——和第 4 步靠 cookie 放行是同一个道理,
只是这次 cookie 里装的是签过名、会过期的 JWT,而不是一个静态口令。authorize
对所有路径统一校验,/ws 也在内,不用再写一段。
两个密钥别弄混,也别泄漏。签名密钥(crypto key)泄漏等于别人能伪造任意登录;
密码哈希用 caddy hash-password 生成、以 bcrypt:10: 开头填进
password。token lifetime 到期后需要重新登录——这正是它比静态 cookie 强的地方:
抓到一个 token 也只在有效期内能用。
caddy-security 还能接 GitHub / Google 登录(OAuth)、加两步验证(TOTP / Yubikey)。 考虑到这服务给的是 shell,2FA 很值得开。完整选项见 它的文档。