❝原文链接:https://icloudnative.io/posts/tailscale-acls/
或者点击左下角的 阅读原文 直接查看原文
大家好,我是米开朗基杨。
前面几篇文章给大家给介绍了 Tailscale 和 Headscale,包括 Headscale 的安装部署和各个平台客户端的接入,以及如何打通各个节点所在的局域网。同时还介绍了如何自建私有的 DERP 服务器,并让 Tailscale 使用我们自建的 DERP 服务器。
还没看的赶紧去看!
芜湖,Tailscale 开源版本让你的 WireGuard 直接起飞~
自建 DERP 中继服务器,从此 Tailscale 畅通无阻
今天我们来探索一下更复杂的场景。想象有这么一个场景,我系统通过 Tailscale 方便的连接一台不完全属于我的设备, 这台设备可能还有其他人也在使用。如果我仅仅是安装一个 Tailscale, 那么所有能登录这台设备的人都可以通过 Tailscale 连接我所有的设备。
我能不能实现这样一种需求:我可以连接这台节点,但是这台节点不能连接我的其他节点?
这就是 Tailscale ACL(Access Control List)干的事情。ACL 可以严格限制特定用户或设备在 Tailscale 网络上访问的内容。
❝虽然 Headscale 兼容 Tailscale 的 ACL,但还是有些许差异的。本文所讲的 ACL 只适用于 Headscale,如果你使用的是官方的控制服务器,有些地方可能跟预期不符,请自行参考 Tailscale 的官方文档。
Tailscale/Headscale 的默认访问规则是 default deny
,也就是黑名单模式,只有在访问规则明确允许的情况下设备之间才能通信。所以 Tailscale/Headscale 默认会使用 allowall
访问策略进行初始化,该策略允许加入到 Tailscale 网络的所有设备之间可以相互访问。
Tailscale/Headscale 通过使用 group 这种概念,可以只用非常少的规则就能表达大部分安全策略。除了 group 之外,还可以为设备打 tag 来进一步扩展访问策略。结合 group 和 tag 就可以构建出强大的基于角色的访问控制(RBAC)策略。
关于 Tailscale 访问控制系统的详情可以参考这篇文章:基于角色的访问控制(RBAC):演进历史、设计理念及简洁实现[1]。这篇文章深入探讨了访问控制系统的历史,从设计层面分析了 DAC -> MAC -> RBAC -> ABAC
的演进历程及各模型的优缺点、适用场景等, 然后从实际需求出发,一步步地设计出一个实用、简洁、真正符合 RBAC 理念的访问控制系统。
Tailscale ACL 需要保存为 HuJSON 格式,也就是 human JSON[2]。HuJSON 是 JSON 的超集,允许添加注释以及结尾处添加逗号。这种格式更易于维护,对人类和机器都很友好。
❝Headscale 除了支持 HuJSON 之外,还支持使用 YAML 来编写 ACL。本文如不作特殊说明,默认都使用 YAML 格式。
Headscale 的 ACL 策略主要包含以下几个部分:
acls
:ACL 策略定义。
groups
:用户的集合。Tailscale 官方控制器的“用户”指的是登录名,必须是邮箱格式。而 Headscale 的用户就是 namesapce。
hosts
:定义 IP 地址或者 CIDR 的别名。
tagOwners
:指定哪些用户有权限给设备打 tag。
autoApprovers
:允许哪些用户不需要控制端确认就可以宣告 Subnet 路由和 Exit Node。
acls 部分是 ACL 规则主体,每个规则都是一个 HuJSON 对象,它授予从一组访问来源到一组访问目标的访问权限。
所有的 ACL 规则最终表示的都是允许从特定源 IP 地址到特定目标 IP 地址和端口的流量。虽然可以直接使用 IP 地址来编写 ACL 规则,但为了可读性以及方便维护,建议使用用户、Group 以及 tag 来编写规则,Tailscale 最终会将其转换为具体的 IP 地址和端口。
每一个 ACL 访问规则长这个样子:
- action: accept
src:
- xxx
- xxx
- ...
dst:
- xxx
- xxx
- ...
proto: protocol # 可选参数
Tailscale/Headscale 的默认访问规则是 default deny
,也就是黑名单模式,只有在访问规则明确允许的情况下设备之间才能通信。所以 ACL 规则中的 action
值一般都写 accept
,毕竟默认是 deny 嘛。
src
字段表示访问来源列表,该字段可以填的值都在这个表格里:
类型 | 示例 | 含义 |
---|---|---|
Any | * | 无限制(即所有来源) |
用户(Namespace) | dev1 | Headscale namespace 中的所有设备 |
Group (ref)[3] | group:example | Group 中的所有用户 |
Tailscale IP | 100.101.102.103 | 拥有给定 Tailscale IP 的设备 |
Subnet CIDR (ref)[4] | 192.168.1.0/24 | CIDR 中的任意 IP |
Hosts (ref)[5] | my-host | hosts 字段中定义的任意 IP |
Tags (ref)[6] | tag:production | 分配指定 tag 的所有设备 |
Tailnet members | autogroup:members | Tailscale 网络中的任意成员(设备) |
proto
字段是可选的,指定允许访问的协议。如歌不指定,默认可以访问所有 TCP 和 UDP 流量。
proto
可以指定为 IANA IP 协议编号[7] 1-255(例如 16)或以下命名别名之一(例如 sctp):
协议 | proto | IANA 协议编号 |
---|---|---|
Internet Group Management (IGMP) | igmp | 2 |
IPv4 encapsulation | ipv4, ip-in-ip | 4 |
Transmission Control (TCP) | tcp | 6 |
Exterior Gateway Protocol (EGP) | egp | 8 |
Any private interior gateway | igp | 9 |
User Datagram (UDP) | udp | 17 |
Generic Routing Encapsulation (GRE) | gre | 47 |
Encap Security Payload (ESP) | esp | 50 |
Authentication Header (AH) | ah | 51 |
Stream Control Transmission Protocol (SCTP) | sctp | 132 |
只有 TCP、UDP 和 SCTP 流量支持指定端口,其他协议的端口必须指定为 *
。
dst 字段表示访问目标列表,列表中的每个元素都用 hosts:ports
来表示。hosts 的取值范围如下:
类型 | 示例 | 含义 |
---|---|---|
Any | * | 无限制(即所有访问目标) |
用户(Namespace) | dev1 | Headscale namespace 中的所有设备 |
Group (ref)[8] | group:example | Group 中的所有用户 |
Tailscale IP | 100.101.102.103 | 拥有给定 Tailscale IP 的设备 |
Hosts (ref)[9] | my-host | hosts 字段中定义的任意 IP |
Subnet CIDR (ref)[10] | 192.168.1.0/24 | CIDR 中的任意 IP |
Tags (ref)[11] | tag:production | 分配指定 tag 的所有设备 |
Internet access (ref)[12] | autogroup:internet | 通过 Exit Node 访问互联网 |
Own devices | autogroup:self | 允许 src 中定义的来源访问自己(不包含分配了 tag 的设备) |
Tailnet devices | autogroup:members | Tailscale 网络中的任意成员(设备) |
ports
的取值范围:
类型 | 示例 |
---|---|
Any | * |
Single | 22 |
Multiple | 80,443 |
Range | 1000-2000 |
groups 定义了一组用户的集合,YAML 格式示例配置如下:
groups:
group:admin:
- "admin1"
group:dev:
- "dev1"
- "dev2"
huJSON 格式:
"groups": {
"group:admin": ["admin1"],
"group:dev": ["dev1", "dev2"],
},
每个 Group 必须以 group:
开头,Group 之间也不能相互嵌套。
autogroup 是一个特殊的 group,它自动包含具有相同属性的用户或者访问目标,可以在 ACL 规则中调用 autogroup。
Autogroup | 允许在 ACL 的哪个字段调用 | 含义 |
---|---|---|
autogroup:internet | dst | 用来允许任何用户通过任意 Exit Node 访问你的 Tailscale 网络 |
autogroup:members | src 或者 dst | 用来允许 Tailscale 网络中的任意成员(设备)访问别人或者被访问 |
autogroup:self | dst | 用来允许 src 中定义的来源访问自己 |
示例配置:
acls:
# 允许所有员工访问自己的设备
- action: accept
src:
- "autogroup:members"
dst:
- "autogroup:self:*"
# 允许所有员工访问打了标签 tag:corp 的设备
- action: accept
src:
- "autogroup:members"
dst:
- "tag:corp:*"
Hosts 用来定义 IP 地址或者 CIDR 的别名,使 ACL 可读性更强。示例配置:
hosts:
example-host-1: "100.100.100.100"
example-network-1: "100.100.101.100/24
tagOwners
定义了哪些用户有权限给设备分配指定的 tag。示例配置:
tagOwners:
tag:webserver:
- group:engineering
tag:secure-server:
- group:security-admins
- dev1
tag:corp:
- autogroup:members
这里表示的是允许 Group group:engineering
给设备添加 tag tag:webserver
;允许 Group group:security-admins
和用户(也就是 namespace)dev1 给设备添加 tag tag:secure-server
;允许 Tailscale 网络中的任意成员(设备)给设备添加 tag tag:corp
。
每个 tag 名称必须以 tag:
开头,每个 tag 的所有者可以是用户、Group 或者 autogroup:members
。
autoApprovers
定义了无需 Headscale 控制端批准即可执行某些操作的用户列表,包括宣告特定的子网路由或者 Exit Node。
当然了,即使可以通过 autoApprovers
自动批准,Headscale 控制端仍然可以禁用路由或者 Exit Node,但不推荐这种做法,因为控制端只能临时修改,autoApprovers
中定义的用户列表仍然可以继续宣告路由或 Exit Node,所以正确的做法应该是修改 autoApprovers
中的用户列表来控制宣告的路由或者 Exit Node。
autoApprovers 示例配置:
autoApprovers:
exitNode:
- "default"
- "tag:bar"
routes:
"10.0.0.0/24":
- "group:engineering"
- "dev1"
- "tag:foo"
这里表示允许 default
namespace 中的设备(以及打上标签 tag:bar
的设备)将自己宣告为 Exit Node;允许 Group group:engineering
中的设备(以及 dev1 namespace 中的设备和打上标签 tag:foo
的设备)宣告子网 10.0.0.0/24
的路由。
要想在 Headscale 中配置 ACL,只需使用 HuJSON 或者 YAML 编写相应的 ACL 规则(HuJSON 格式的文件名后缀为 hujson),然后在 Headscale 的配置文件中引用 ACL 规则文件即可。
# Path to a file containg ACL policies.
# ACLs can be defined as YAML or HUJSON.
# https://tailscale.com/kb/1018/acls/
acl_policy_path: "./acl.yaml"
默认的 ACL 规则允许所有访问流量,规则内容如下:
# acl.yaml
acls:
- action: accept
src:
- "*"
dst:
- "*:*"
假设 Headscale 有两个 namesapce:default
和 guest
。管理员的设备都在 default
namespace 中,访客的设备都在 guest
namespace 中。
$ headscale ns ls
ID | Name | Created
1 | default | 2022-08-20 06:15:17
2 | guest | 2022-11-27 09:20:25
$ headscale -n default node ls
ID | Hostname | Name | NodeKey | Namespace | IP addresses | Ephemeral | Last seen | Online | Expired
2 | OpenWrt | openwrt-njprohi0 | [7LdVc] | default | 10.1.0.2, | false | 2022-08-26 04:18:43 | offline | no
5 | tailscale | tailscale-home | [pwlFE] | default | 10.1.0.5, | false | 2022-11-27 10:02:35 | online | no
10 | k3s-worker05 | share | [5Z38M] | default | 10.1.0.9, | false | 2022-11-22 18:49:25 | offline | no
11 | Galaxy a52s | galaxy-a52s-arg5owsh | [U+0qY] | default | 10.1.0.1, | false | 2022-11-27 10:02:34 | online | no
$ headscale -n guest node ls
ID | Hostname | Name | NodeKey | Namespace | IP addresses | Ephemeral | Last seen | Online | Expired
12 | guest-1 | guest-1 | [75qSK] | guest | 10.1.0.10, | false | 2022-11-27 10:05:33 | online | no
13 | guest-2 | guest-2 | [8lONp] | guest | 10.1.0.11, | false | 2022-11-27 10:05:31 | online | no
现在我想让 default
namespace 中的设备可以访问所有设备,而 guest
namespace 中的设备只能访问 guest
namespace 中的设备,那么规则应该这么写:
# acl.yaml
acls:
- action: accept
src:
- "default"
dst:
- "*:*"
- "guest:*"
- action: accept
src:
- "guest"
dst:
- "guest:*"
在 guest-1
上查看 Tailscale 状态:
$ tailscale status
10.1.0.10 ks-node-2 guest linux -
desktop-aoulurh-j7dfnsul.default.example.com default windows offline
galaxy-a52s-arg5owsh.default.example.com default android active; relay "hs", tx 12112 rx 11988
guest-3 guest linux active; direct 172.31.73.176:41641, tx 2552 rx 2440
openwrt-njprohi0.default.example.com default linux offline
tailscale-home.default.example.com default linux active; direct 60.184.243.56:41641, tx 3416 rx 25576
看起来 guest-1
可以看到所有的设备,但事实上它只能 ping 通 guest-2
,我们来验证一下:
$ ping 10.1.0.1
PING 10.1.0.1 (10.1.0.1) 56(84) bytes of data.
^C
--- 10.1.0.1 ping statistics ---
9 packets transmitted, 0 received, 100% packet loss, time 8169ms
果然是 ping 不通的。但是 10.1.0.1 这个设备是可以反向 ping 通 guest-1 的:
# 在 10.1.0.1 所在的设备操作
$ ping 10.1.0.10
PING 10.1.0.10 (10.1.0.10) 56(84) bytes of data.
64 bytes from 10.1.0.10: icmp_seq=1 ttl=64 time=68.9 ms
64 bytes from 10.1.0.10: icmp_seq=2 ttl=64 time=91.5 ms
64 bytes from 10.1.0.10: icmp_seq=3 ttl=64 time=85.3 ms
64 bytes from 10.1.0.10: icmp_seq=4 ttl=64 time=79.7 ms
^C
--- 10.1.0.10 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss, time 3005ms
rtt min/avg/max/mdev = 68.967/81.389/91.551/8.306 ms
ssh 测试一下:
$ ssh [email protected]
[email protected]'s password:
完美。
下面再来看看 guest-1
能不能 ping 通 guest-2
:
# 在 guest-1 设备上操作
$ ping 10.1.0.11
PING 10.1.0.11 (10.1.0.11) 56(84) bytes of data.
64 bytes from 10.1.0.11: icmp_seq=1 ttl=64 time=2.93 ms
64 bytes from 10.1.0.11: icmp_seq=2 ttl=64 time=1.33 ms
^C
--- 10.1.0.11 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 1.325/2.128/2.931/0.803 ms
和我在上面预测的效果一样,ACL 规则生效了。
tag 有一个非常神奇的功效:它可以让 src
和 dst
中的元素失效。具体什么意思呢?假设你的 src 或 dst 中指定了 namespace 或者 group,那么这个规则只对这个 namespace 或者 group 中(没有分配 tag 的设备)生效。
举个例子你就明白了,现在我给 guest-2 打上一个 tag:
$ headscale node tag -i 13 -t tag:test
Machine updated
$ headscale -n guest node ls -t
ID | Hostname | Name | NodeKey | Namespace | IP addresses | Ephemeral | Last seen | Online | Expired | ForcedTags | InvalidTags | ValidTags
12 | ks-node-2 | ks-node-2 | [75qSK] | guest | 10.1.0.10, | false | 2022-11-27 10:18:35 | online | no | | |
13 | ks-node-3 | ks-node-3 | [8lONp] | guest | 10.1.0.11, | false | 2022-11-27 10:18:31 | online | no | tag:test | |
此时 guest-1 就 ping 不通 guest-2 了:
# 在 guest-1 设备上操作
$ ping 10.1.0.11
PING 10.1.0.11 (10.1.0.11) 56(84) bytes of data.
^C
--- 10.1.0.11 ping statistics ---
4 packets transmitted, 0 received, 100% packet loss, time 3070ms
这就说明 guest-2 并不包含在 guest:*
这个访问目标中,也就是说打了 tag 的设备并不包含在 guest:*
这个访问目标中。
此时其他设备如果还想继续 guest-2,必须在 dst 中指定 tag:test
:
acls:
- action: accept
src:
- "default"
dst:
- "*:*"
- "guest:*"
- "tag:test:*"
- action: accept
src:
- "guest"
dst:
- "guest:*"
- "tag:test:*"
再次测试访问:
# 在 guest-1 设备上操作
$ ping 10.1.0.11
PING 10.1.0.11 (10.1.0.11) 56(84) bytes of data.
64 bytes from 10.1.0.11: icmp_seq=1 ttl=64 time=1.31 ms
64 bytes from 10.1.0.11: icmp_seq=2 ttl=64 time=3.40 ms
^C
--- 10.1.0.11 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1002ms
rtt min/avg/max/mdev = 1.314/2.355/3.397/1.041 ms
果然可以 ping 通了。
Tailscale/Headscale 的 ACL 非常强大,你可以基于 ACL 实现各种各样的访问控制策略,本文只是给出了几个关键示例,帮助大家理解其用法,更多功能大家可以自行探索(比如 group 等)。下篇文章将会给大家介绍如何配置 Headscale 的 Exit Node,以及各个设备如何使用 Exit Node,届时会用到 ACL 里面的 autoApprovers
,敬请期待!
[1]
基于角色的访问控制(RBAC):演进历史、设计理念及简洁实现: http://arthurchiao.art/blog/rbac-as-it-meant-to-be-zh/
[2]human JSON: https://github.com/tailscale/hujson
[3](ref): https://tailscale.com/kb/1018/acls/#groups
[4](ref): https://tailscale.com/kb/1019/subnets
[5](ref): https://tailscale.com/kb/1018/acls/#hosts
[6](ref): https://tailscale.com/kb/1068/acl-tags
[7]IANA IP 协议编号: https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml
[8](ref): https://tailscale.com/kb/1018/acls/#groups
[9](ref): https://tailscale.com/kb/1018/acls/#hosts
[10](ref): https://tailscale.com/kb/1019/subnets
[11](ref): https://tailscale.com/kb/1068/acl-tags
[12](ref): https://tailscale.com/kb/1103/exit-nodes
你可能还喜欢
点击下方图片即可阅读
Kubernetes 上千规模 Pod 最佳实践
2022-11-25
博通收购 VMware 计划受阻,英国监管机构介入
2022-11-24
深入理解 Kubernetes 中的用户与身份认证授权
2022-11-23
彻底解决 K8s 节点本地存储被撑爆的问题
2022-11-22
云原生是一种信仰
关注公众号
后台回复◉k8s◉获取史上最方便快捷的 Kubernetes 高可用部署工具,只需一条命令,连 ssh 都不需要!
点击 "阅读原文" 获取更好的阅读体验!
发现朋友圈变“安静”了吗?