Tailscale ACL 实战:让生产服务器只能被访问、不能主动出站

目录

在AI时代,更安全的运维做法。让生产服务器只能被访问、不能主动出站,不能访问你的笔记本,这样在被黑客攻破时,你不会泄露更多信息

需求

我在 tailnet 里有几台生产服务器,统一打上 tag:prod-server。目标很简单:我的设备可以访问它们,但它们不能主动访问 tailnet 里的任何设备。这样即使某台生产机被攻破,攻击者也没法借 Tailscale 横向摸到我的笔记本或其他机器。

配置本身只有几行,但过程中踩了三个坑:默认策略的全放通规则、Tailscale SSH 的独立权限,以及出口节点失效。下面按顺序记录。

先搞清楚:默认拒绝,但默认策略是全放通

Tailscale 的规则引擎是默认拒绝的:没有被任何规则允许的流量都会被丢弃,所以只有 accept,没有 deny。

但新建 tailnet 自带的策略里有一条全放通规则:

json
{ "src": ["*"], "dst": ["*"], "ip": ["*"] }

所以开箱即用时所有设备能互访,这是这条规则的效果,不是引擎默认放行。而 * 作为源包括打了 tag 的设备,只要它还在,prod-server 就能访问所有人。

另外一点:连接是有状态的。别人访问 prod-server 时它的回包会正常放行,不需要为它写任何出站规则。因此“只能被访问”的做法就是:策略里不出现任何以 tag:prod-server 为源的规则。

网络层配置

删掉全放通规则后,把需要的访问关系显式写回来:

jsonc
"grants": [
  // 每个用户自己的设备之间互通
  { "src": ["autogroup:member"], "dst": ["autogroup:self"],     "ip": ["*"] },
  // 成员可以访问 prod-server(可按需收紧到 tcp:22、tcp:443)
  { "src": ["autogroup:member"], "dst": ["tag:prod-server"],   "ip": ["*"] },
]

关键是源用 autogroup:member 而不是 *。打了 tag 的设备不再属于任何用户,不算 member,所以不会被包含进去。注意 autogroup:self 只覆盖自己名下的设备:多用户时 A 访问不了 B 的设备,所有 tag 设备也都不在其中。

坑一:SSH 被拒

改完之后 ssh sg-server 报错:

text
tailscale: tailnet policy does not permit you to SSH to this node

网络是通的,拦住我的是 Tailscale SSH。服务器开了 tailscale up --ssh 后,22 端口由 Tailscale 接管,它除了 grants 之外还要求策略的 ssh 段允许这次登录。默认的 ssh 规则目标是 autogroup:self,而 sg-server 打上 tag 后已不属于我,所以被拒。

补一条规则即可:

jsonc
"ssh": [
  { "action": "check", "src": ["autogroup:member"], "dst": ["autogroup:self"],
    "users": ["autogroup:nonroot", "root"] },
  { "action": "check", "src": ["autogroup:admin"],  "dst": ["tag:prod-server"],
    "users": ["root", "autogroup:nonroot"] },
]

生产机用 autogroup:admin 作源,只让管理员登录;check 会定期要求浏览器重新认证,比 accept 更稳妥。

坑二:出口节点用不了

其中一台 prod-server 同时是出口节点,改完后也失效了。原因不是打了 tag,而是删掉了全放通规则:只有能访问 autogroup:internet 的设备才能使用出口节点,原来的 * 隐式包含了它。

补一条 grant:

jsonc
{ "src": ["autogroup:member"], "dst": ["autogroup:internet"], "ip": ["*"] }

这条只放行我的设备经由出口节点上网,不给 prod-server 任何出站权限。如果还不行,再检查两点:

  • 用 sudo tailscale set --advertise-exit-node 确认出口节点通告还在。tailscale up 需要带全部参数,只写 --advertise-tags 可能把它丢掉。
  • 在后台确认 “Use as exit node” 仍被批准,或在策略里加 "autoApprovers": { "exitNode": ["tag:prod-server"] }。

不过生产机兼做出口节点会让个人流量经过它,条件允许的话单独用一台机器更干净。

prod-server 之间要不要互通

按上面的配置,prod-server 之间既不能互相访问,也不能互相 SSH。如果业务需要(比如应用连数据库),可以只开必要的端口:

jsonc
// grants
{ "src": ["tag:prod-server"], "dst": ["tag:prod-server"], "ip": ["tcp:5432"] }

// ssh:机器之间没人做浏览器认证,用 accept
{ "action": "accept", "src": ["tag:prod-server"], "dst": ["tag:prod-server"], "users": ["deploy"] }

即使这样,prod-server 仍然碰不到个人设备,而且 Tailscale SSH 本身禁止以 tag 为源 SSH 到用户拥有的设备。代价是一台被攻破后可以横向移动到同类机器,所以没有实际需求就别开。

偷懒写法与最终配置

如果 tailnet 只有自己一个人,可以把 grants 简化成一条:

jsonc
{ "src": ["autogroup:member"], "dst": ["*"], "ip": ["*"] }

dst: * 包括所有设备、子网路由和 autogroup:internet,出口节点也随之恢复;源仍是 member,prod-server 依然无法出站。代价是权限变宽:用户之间可以互访,以后新加的 tag 设备和子网默认就能被访问。多人使用时还是推荐分开写,让每个权限都是显式的。

最后,用 tests 把约束锁死,以后误改策略时保存会直接报错:

jsonc
"tests": [
  { "src": "tag:prod-server", "deny": ["autogroup:member:22", "tag:prod-server:22"] },
],
"sshTests": [
  { "src": "[email protected]", "dst": ["tag:prod-server"], "accept": ["root"] },
],

总结一句:删掉全放通、源只用 autogroup:member、为 SSH 和出口节点各补一条规则。

参考

评论