运维 | Tailscale 的默认全放行:一次把家里的门关上近期更新
运维 | Tailscale 的默认全放行:一次把家里的门关上
有些默认值是善意:让你第一分钟就能把两台机器连起来。代价总要有人付,只是账单晚一点到。
写在前面
夜里刷到一条推文,讲的是用 Tailscale 同时连 VPS 和家里的设备。作者的提醒很直白:默认的 * -> * 全放行,会让名册上所有设备彻底互通——某天 VPS 被拿下,家里的 NAS 也就跟着开门。
看完的第一反应不该是转发,是登进后台看一眼自己那份策略。
结果不出意外:我从没动过它。
一、先把默认值翻译成人话
后台 Access controls 里那段 JSON,如果从没手动改过,核心就是一行:
{
"grants": [
{ "src": ["*"], "dst": ["*"], "ip": ["*"] }
]
}文档里有一句值得抄下来:In the absence of an acls section, Tailscale applies the default allow all policy. —— "我从没配过"这件事本身,就是一份允许一切的策略。 名册上几台机器,一台 tag 都没有,它们以"同一个用户的设备"的身份彼此全通。
二、威胁模型:哪一边才是暴露面
组网工具容易制造一种错觉:既然都叫"内网",彼此就都可信。实际上不是。
有公网 IP、跑着反向代理、对外提供服务的机器,才是唯一"可能先被别人拿下"的那一类;家里的 NAS、台式机、智能家居网关没有入站口,它们唯一的风险,是有人从内部把门打开。
于是威胁模型很清楚:
| 角色 | 定位 |
|---|---|
| 公网 VPS | 暴露面,随时假设"已不属于你" |
| 家里的设备 | 高价值资产:面板、下载器、媒体库、一台开着远程桌面的 Windows |
| 自己的手机 / 笔记本 | 需要全权,日常还要用 |
一台 VPS 被拿下之后,从隧道里能摸到的东西是这样的:
| 路径 | 后果 |
|---|---|
| → 家里 NAS 的管理面板与下载器 | 面板被接管,媒体库被拖走 |
→ 家里的台式机(3389 / 445) | RDP 与 SMB,等于整机钥匙 |
| → 智能家居网关 | 家庭设备被控 |
三、方案:只给暴露面打 tag
核心只有一句:打了 tag 的节点不再属于"你的设备"这个 autogroup,于是它自动掉出那条全放行规则;而没打 tag 的家内设备什么都不用动,靠"默认拒绝"被保护。
最终策略只有三条规则:
{
"tagOwners": { "tag:vps": ["autogroup:admin"] },
"grants": [
{ "src": ["autogroup:member"], "dst": ["autogroup:self"], "ip": ["*"] },
{ "src": ["autogroup:member"], "dst": ["tag:vps"], "ip": ["*"] },
{ "src": ["tag:vps"], "dst": ["tag:vps"], "ip": ["tcp:22", "icmp:*"] }
]
}逐条看:
- 你的设备之间互通 —— 手机看面板、笔记本远程桌面,全在这个 autogroup 里,日常体验零变化;
- 你 → VPS 全端口 —— 从 VPS 拉中转下载的文件也走这条;
- VPS ↔ VPS —— 只留 SSH 互救和 ICMP。
刻意没有的那一条才是重点:tag:vps 到任何家内设备。默认拒绝。
两个细节值得单独记:
- ICMP 必须留。只放 TCP 会让
tailscale ping全线失败,之后的排障就只剩猜。 tailscale ssh也吃 ACL。VPS 打了 tag 就会掉出member → self,得在ssh段把tag:vps补进 dst —— 否则某天你会忽然发现 SSH 进不去,还以为是密钥坏了。
四、用 API 改,而不是在后台点
在网页上点保存当然更快,但改之前你要有一份原文、改完要能一条命令回去。后台的保存按钮不提供这两样。
先备份。Accept 头决定拿回来的是 huJSON 原文,而不是被规范化过的 JSON:
TOK=$(cat /path/to/token) # 后台 Keys → API access tokens
curl -sS -u "$TOK:" -H 'Accept: application/hujson' \
https://api.tailscale.com/api/v2/tailnet/-/acl > policy.backup.hujson然后是四步:只验证 → 正式写入 → 给设备打 tag → 带上自检再写一次。
# 1. 只验证,不生效(返回 {} 即通过)
curl -sS -u "$TOK:" -H 'Content-Type: application/hujson' -X POST \
--data-binary @policy.hujson \
https://api.tailscale.com/api/v2/tailnet/-/acl/validate
# 2. 正式写入
curl -sS -u "$TOK:" -H 'Content-Type: application/hujson' -X POST \
--data-binary @policy.hujson \
https://api.tailscale.com/api/v2/tailnet/-/acl
# 3. 给某台设备打 tag(设备 ID 从 /tailnet/-/devices 拿)
curl -sS -u "$TOK:" -H 'Content-Type: application/json' -X POST \
-d '{"tags":["tag:vps"]}' \
https://api.tailscale.com/api/v2/device/<设备ID>/tags五、四个坑,一个没落
1. 状态码 200 不代表成功
/acl 和 /acl/validate 的 body 是策略文件原文(application/hujson),不是 {"acl": "..."}。包一层会拿到 {"message":"json: unknown field \"acl\""} —— HTTP 状态码依然是 200。排错时先看 body,别信状态码。
2. tag 和自检互相依赖
策略里还没有 tagOwners 时给设备打 tag,返回的是 requested tags [...] are invalid or not permitted;而自检里引用 tag:vps,又要求名册上已经有节点带着这个 tag,否则判定为 want: Accept, got: Drop,保存直接被拒。
顺序只能是:先发"有 tagOwners、没有自检"的版本 → 打 tag → 再发带自检的版本。想一步到位,会被这两道闸门夹在中间。
3. 自检里的 src 不能写 autogroup
"src": "autogroup:member" 会报 unknown user or host,得换成真实账号;accept / deny 里用 IP:端口 最稳。
4. 第一版方案其实是多余的
我最早想给家里的设备也打一组 tag:home,写完才发现完全没必要:未打 tag 的设备本来就落在你自己的 autogroup 里,默认拒绝已经保护它们。多打一组 tag,只是多一组未来会漂移、会在重建容器时丢失的状态。
能靠"没有规则"解决的问题,别用规则解决。
六、验证:让策略引擎自己回答
自检(tests)不是注释。保存时它会真的跑一遍断言,不通过就不允许入库。
所以最好的验证不是"我去 ping 一下试试",而是让控制面替我回答:
"tests": [
{
"src": "<你的账号>",
"accept": ["<自用设备>:22", "<台式机>:3389"]
},
{
"src": "tag:vps",
"accept": ["<另一台VPS>:22"],
"deny": [
"<家内NAS>:22",
"<家内NAS>:<面板端口>",
"<台式机>:3389",
"<台式机>:445"
]
}
]保存返回 {} 的那一刻,引擎已经确认了两件事:你自己的设备还能到家里;而 tag 里的节点到 NAS 面板、到台式机 RDP 与 SMB,判定全是 Drop。
这比端到端探测更可靠——探测会受网络、防火墙、服务是否在听的影响,而这里是策略引擎对规则本身的判决。
七、两个顺带的发现
- 名册上没有任何节点宣告
0.0.0.0/0,也就是说"别把 VPS 当 exit node"这条天然满足。不是因为我谨慎,是因为从没配过——这两件事的结局常常一样,性质完全不同。 - 名册上还挂着一台两年前建的节点,最后一次上线停在当年秋天。僵尸身份不会自己消失,它只是安静地躺在全放行名单里。
八、留在最后
安全这件事,最贵的从来不是配置,是"我以为配过"。
默认值是延后支付的账单:平时看不见,出事那天一次性结清。而"没配过"这三个字,在权限系统里从来不是空白的含义——它是一份策略,只是写的人不是你。
2026 年 11 月,于一间深夜还亮着灯的屋子。全部操作由 Hermes Agent 辅助完成。
最后更新于 2026-11-08 「部分内容存在时效性,如有失效请留言反馈」
除注明外为 袅残烟的冥想盆 原创文章,转载请注明出处。
本作品采用 知识共享署名-相同方式共享 4.0 国际许可协议 进行许可。