Tailscale Serve:在 tailnet 内发布 HTTPS 服务
Tailscale Serve 是 tailnet 内的内置反向代理,把本地 HTTP 服务以自动签发的 HTTPS 证书发布给 tailnet 设备,免域名、免公网暴露。讲清它的 TLS 终止、证书来源、与 Caddy/Cloudflare Tunnel 的差异,以及 443 被占用、重启失效等坑。
[!info] related notes
- 远程浏览器场景:在 Oracle 上部署远程浏览器实战
- 远程浏览器安全收口总论:远程浏览器的安全访问
- 接入 tailnet:在 OCI VPS 上接入 Tailscale
- TLS 排错方法论:TLS 握手失败排查
Tailscale Serve:在 tailnet 内发布 HTTPS 服务
一句话定义
Tailscale Serve 是 Tailscale 客户端自带的反向代理:它把本机的一个 HTTP 服务,用 Tailscale 自动签发的 HTTPS 证书,发布给同一 tailnet 内的设备访问。
它解决的是一个听起来很普通、实则处处是坑的问题:怎么给一个只跑 HTTP 的本地服务(比如 noVNC、Web 面板)加上可被浏览器信任的 HTTPS,同时不暴露公网?
为什么需要它:HTTPS 的隐含前提
一个”能被浏览器信任的 HTTPS”必须解决两件事:
- TLS 终止:把加密流量解开,交给后面的 HTTP 服务;
- 可信证书:证明”我是 example.com / xxx.ts.net”,且证书来自浏览器信任的 CA。
第 2 点是硬门槛——证书不可能凭空出现。常规路线要么买域名 + Let’s Encrypt,要么自己造自签证书(浏览器弹警告)。而 Tailscale Serve 直接白送了第 2 点:它用 Tailscale 自己的 CA 给 *.ts.net 域名签发证书,tailnet 内的设备天然信任。
[!tip] 所以”容器外还要跑一条命令”不是冗余,而是把”证书 + TLS 终止 + 安全隔离”这一整坨本来要做的事,压缩成一条命令。
工作原理
flowchart LR
A[你的浏览器<br/>tailnet 内] -->|HTTPS 自动证书 *.ts.net| B[tailscaled<br/>Serve 代理]
B -->|HTTP 本地回环| C[本地服务<br/>127.0.x.x:3010]
- 浏览器访问
https://<host>.ts.net:<port>,TLS 由 tailscaled 终止(Serve 模块); - tailscaled 把请求以明文 HTTP 转发到本机的
127.0.x.x:<localport>; - 证书由 Tailscale CA 自动签发,tailnet 设备已内置该 CA,浏览器不报警;
- 默认
tailnet only:只有 tailnet 内设备可达,公网不可见。
基础命令
# 把本地 3010 以 HTTPS 发布到 tailnet,对外用 8443 端口
sudo tailscale serve --bg --https=8443 http://127.0.x.x:3010
# 查看状态
sudo tailscale serve status
# 关闭
sudo tailscale serve off
参数说明:
--https=<port>:对外 HTTPS 监听端口(不写则默认 443);--bg:后台常驻;不加则前台运行,进程被杀后配置即失效;- 目标
http://127.0.x.x:<port>是 Serve 要转发的本地 HTTP 服务。
与另外两种 HTTPS 收口方案对比
| 维度 | Tailscale Serve | Caddy / Nginx 反代 | Cloudflare Tunnel |
|---|---|---|---|
| 需要自己的域名 | ❌ 不需要(*.ts.net 自带) | ✅ 需要 | ✅ 需要(托管到 CF) |
| 证书来源 | Tailscale CA 自动签 | Let’s Encrypt 自动 | Cloudflare 边缘证书 |
| 公网暴露 | ❌ tailnet only,零公网端口 | ⚠️ 要开 443 入站 | ❌ 仅出站隧道 |
| 客户端要求 | 装 Tailscale + 同 tailnet | 任意浏览器 | 任意浏览器 |
| 配置量 | 一条命令 | 一份 Caddyfile + DNS | cloudflared + 后台开关 |
| 大陆可达性 | WireGuard,偶尔走 DERP 中继 | 直连公网 | CF 免费 anycast 常被墙/限速 |
[!note] 选型直觉
- 只想自己/小团队安全访问 → Tailscale Serve 最省事;
- 想用自定义域名 + 公共浏览器访问 → Caddy(需域名);
- 想套 Cloudflare Access 统一鉴权 → Cloudflare Tunnel(但大陆连通性需实测)。
实战中的坑(都已踩过)
坑 1:443 端口被占用 → 改用其它端口
tailscale serve 默认监听 443。若本机 443 已被其它容器/服务占用(如另一 Docker 容器的 docker-proxy),Serve 会撞到那个服务的错证书,报 TLS alert, internal error——这不是 tailscaled 的 bug,是 443 被抢。
[!fix] 解法 改用空闲端口(如
8443)即可:sudo tailscale serve --bg --https=8443 http://127.0.x.x:3010访问地址变为
https://<host>.ts.net:8443。
坑 2:需 tailnet 管理员先启用 Serve 功能
首次在节点上 tailscale serve 可能被 tailnet 的「Serve 功能开关」挡住,提示去后台点启用。这一步必须人工在浏览器点(链接形如 https://login.tailscale.com/f/serve?node=...)。
坑 3:--bg 忘了加 → 配置随进程退出消失
不加 --bg 时 Serve 跑在前台,我的 SSH 命令一结束(或被 timeout 杀掉),代理进程就没了,下次 serve status 为空。务必加 --bg。
坑 4:证书首次签发需要等几秒
Serve 起来后立刻 curl 可能仍报 TLS internal error——证书是首次现签的。等 10~30 秒再测,或显式触发一次 sudo tailscale cert <host>.ts.net 加速。
坑 5:重启后 Serve 多半要重跑
--bg 进程不是 systemd 托管的,机器重启后 Serve 不会自动恢复。见下文的固化方案。
固化:让 Serve 开机自启
把 Serve 写成 systemd 服务,避免每次手动跑:
# /etc/systemd/system/tailscale-serve-browser.service
[Unit]
Description=Tailscale Serve for remote browser
After=network-online.target tailscaled.service
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/bin/tailscale serve --bg --https=8443 http://127.0.x.x:3010
ExecStop=/usr/bin/tailscale serve off
User=root
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now tailscale-serve-browser
[!warning] 注意 Tailscale IP 漂移 若容器绑死在某个 Tailscale IP(如
100.68.x.x),而该 IP 日后变更,容器会因绑不到旧 IP 起不来。届时在节点上tailscale ip -4取新 IP,改docker-compose.yml后docker compose up -d,Serve 目标也要相应调整。更稳妥的做法是让容器绑127.0.x.x,由 Serve 收口(见 部署实战)。
Related notes
- 远程浏览器实战:在 Oracle 上部署远程浏览器实战
- 安全收口总论:远程浏览器的安全访问
- 接入 tailnet:在 OCI VPS 上接入 Tailscale
- TLS 排错:TLS 握手失败排查