众所周知,OpenStack 安全组默认是通过 Linux iptables 实现的,不过发现目前还是很少有深入细节解析 OpenStack 安全组实现,于是在下班时间花了几个小时时间重新梳理了下,顺便记录下。
1 iptables 简介
1.1 iptables 概述
在介绍 OpenStack 安全组前先简单介绍下 iptables,其实 iptables 只是一个用户空间的程序,真正干活的其实是 Linux 内核 netfilter,通过 iptables 创建新规则,其实就是在 netfilter 中插入一个 hook,从而实现修改数据包、控制数据包流向等,对 iptables 使用方法不熟悉的可以参考图文并茂理解iptables.
简单地说,iptables 就是通过一系列规则条件匹配执行指定的动作,因此一条规则就是由条件+动作构成,条件比如源 IP 地址、四层协议、端口等,动作如拒绝、通过、丢弃、修改包等,动作通常通过-j
参数指定。
比如拒绝 192.168.1.2 访问目标 22 端口,只需要添加如下 iptables 规则:
如上:
-t
指定表(table),如果把所有的规则混放在一起肯定会特别乱,因此 iptables 根据功能划分为不同的表,过滤包的放在 filter 表,做 NAT 的放 nat 表等,还有 raw 表、mangle 表、security 表,共 5 个表。如果不指定该参数,默认会选中 filter 表。-I
表示 insert 操作,在最前面插入这条规则,相对应的还有-A
参数,表示从末尾追加规则,-I
、-A
还可以在后面指定索引位置,将规则插入到指定的位置。INPUT
表示链名称,链可以看做是一个链表,链表元素为规则。iptables 一共可操纵 5 条链,分别为PREROUTING
、INPUT
、FORWARD
、OUTPUT
、POSTROUTING
。需要注意的是,所有的表都是共享这 5 条链的,当然并不是所有的表都同时需要这 5 条链,比如 filter 表就没有PREROUTING
、POSTROUTING
。如果多个 table 都在如上链上插入了规则,则根据raw -> mangle -> nat -> filter
的顺序执行。-s
、-p
、--dport
都是条件,多个条件是与
的关系,即只有满足指定的所有条件才能匹配该规则,如上-s
指定了源地址 IP 为192.168.1.2
,-p
指定了协议为TCP
,--dport
指定了端口 22,即只有源地址访问目标的 22 TCP 端口才能匹配这条规则。-j
指定了行为,当然官方的叫法是目标(target),这里DROP
表示丢弃包。
1.2 iptables 匹配条件
除了以上的-s
、-p
、--dport
等参数作为匹配条件外,iptables 还支持如-d
匹配目标 IP 地址,-i
、-o
分别指定从哪个网卡进入的以及从哪个网卡出去的。当然这些匹配条件还不够,甚至都不支持匹配 MAC 地址。iptables 为了满足不同的需求,通过扩展模块支持更多的匹配条件,主要分为如下两类:
功能加强型:比如前面的
--dport
参数只能匹配单个 port 或者连续的 port,如果需要匹配多个不连续的 port,则不得不通过添加多条规则实现。mulport
扩展模块允许同时指定多个 port,通过逗号分隔。再比如ip-range
模块,支持指定 ip 地址段。新功能:比如
mac
模块支持匹配源 MAC 地址。time
模块支持通过时间段作为匹配条件,比如实现每天 0 点到 8 点不允许外部 SSH。
不同的扩展模块支持不同的参数,比如mac
模块,支持--mac-source
参数。
使用扩展模块必须通过-m
参数加载,之前我一直以为-m
是--module
的缩写,看 iptables 的 man 手册才发现其实是--match
的缩写,不过我们只需要知道是加载扩展模块的功能就可以了。
比如我们不允许 MAC 地址为FA:16:3E:A0:59:BA
通过,通过如下规则配置:
iptables 的扩展模块非常多,具体可以通过man iptables-extensions
命令查看,不过 OpenStack 安全组用到的并不多:
comment
:给规则添加注释。tcp
/udp
/icmp
:没错,这些也属于扩展模块,iptables 基本模块中甚至连指定端口的功能都没有。set
: 匹配 ipset,当 ip 在 ipset 集合中即满足条件。mac
:前面说了,支持匹配 MAC 地址。state
: 这个模块非常有用,举个简单的例子,假设服务器 A(192.168.0.1)配置的 iptables 规则为入访全不通,即 INPUT 链全 DROP,出访全通,即 OUTPUT 链全 ACCEPT。另外一台服务器 B(192.168.0.2)和 A 在同一个二层网络,则显然 B ping 不通 A,问题是 A 能 ping 通 B 吗?有人肯定会说,A 既然出访全是通的,那肯定能 ping 通 B 了。事实上,A 根本 ping 不通 B,因为 A 的包有去无回,即 A 的 ICMP 包确实能到 B,但 B 的回包却被 A 的INPUT
DROP 了,因此 A 根本接收不到 reply 包。那怎么解决呢?把 B 加到 A 的白名单列表中显然破坏了我们原有的初衷。通过state
模块可以完美解决这个问题,指定 state 为ESTABLISHED
能够匹配已经建立连接的包,注意这里的已建立连接并不是说 TCP 连接,而是更广泛的连接含义,比如 udp、icmp,简单理解就是匹配回包。因此解决如上问题只需要添加-A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
规则即可。physdev
: 这个模块相对内置的-i
、-o
参数功能更强大。假如我们创建了一个 linux bridgebr0
,br0
上挂了很多虚拟网卡 tap 设备。我们通过-i
指定br0
则不管从哪个虚拟网卡进来的都会匹配,做不了精确匹配到底是从哪个虚拟网卡进来的。而physdev
模块则非常强大,通过physdev-in
参数指定从哪个接口进来的,通过--physdev-out
参数指定从哪个接口出去的。
1.3 iptables 执行动作
前面提到 iptables 通过-j
指定执行的动作(target),iptables 常见的 target 如下:
ACCEPT: 接收包,直接放行,不需要在匹配该链上的其他规则,注意是该链,其他链的还是需要匹配的,即只是说明通了一关,后面几关能不能通过还不好说。
DROP: 直接丢弃包,包都丢了,当然也不需要在匹配其他任何规则了。
REJECT: 拒绝包。这个和 DROP 有什么区别呢?DROP 是直接丢弃包,不做任何响应,客户端会一直在傻傻地等直到超时。而 REJECT 会响应拒绝消息,客户端能收到拒绝包并作出反应,不需要一直盲等。
LOG: 仅仅记录下日志。
当然还有实现 NAT 的 SNAT、MASQUERADE、DNAT,因为安全组实现涉及不到,因此不做详细介绍,另外还有RETURN
以及指向另一个链的动作,等后面介绍了子链再讨论。
动作通常都是短路的,也就是说一旦匹配规则并执行动作,就不会继续往后去匹配该链的其他规则了,当然这并不是绝对的,比如LOG
动作就是例外,执行该动作后会继续匹配下一条规则。
1.4 iptables 链
前面提到 iptables 一共有 5 条链,并且链可以认为是一个单向链表,问题来了,当接收到一个新包,到底是如何匹配规则的。这里我直接引用图文并茂理解iptables的图:
(1) 数据包首先到达
PREROUTING
链,然后按照raw
、mangle
、nat
的顺序匹配执行定义在PREROUTING
的规则。(2) 接下来经过路由判断,如果包是发给自己的则流向
INPUT
链,然后由INPUT
链发给用户空间进程处理。如果不是发给自己的包,则流向FORWARD
表,同样按照raw -> mangle -> nat -> filter
表依次匹配执行链上的规则。(3) 同理,
ONPUT
链、POSTROUTING
链,包流向方向,直接看图,非常清晰,这里不再赘述。
前面提到每条链上都可以插入规则,需要注意的是这些规则是有顺序的,iptables 每次匹配时都是从第一条规则开始匹配,依次匹配下一条,一旦匹配其中一条规则,则执行对应的动作。
肯定有人会疑问,如果这条链上的规则都不匹配该怎么办,答案是取决于该链的默认策略(policy)。如果该策略是 DROP,则最后没有匹配的包都将丢弃,即该链时白名单列表。如果默认策略是 ACCEPT,则最后没有匹配的包都会通过,即该链时黑名单列表。当然通常 policy 都设置为ACCEPT
,因为配置为DROP
太危险了,比如清空规则立马就相当于全不通了,如果你通过 SSH 连接的服务器,则立即中断连接了,不得不通过 vnc 或者带外 console 连接重置,所以不建议修改 policy。
通过如下命令查看 filter 表各个链的默认策略:
如果一条链规则特别多且复杂,管理起来非常麻烦,因此很有必要对链根据功能分组。iptables 通过自定义链实现。用户可以通过iptables -N name
创建一个新链,然后和内置链一样可以往新链中添加规则。但是需要注意的是,自定义链不能独立存在,必须挂在内置 5 条链下面,即必须是内置链的子链。
前面 1.3 节提了下-j
可以指定一条新链,这里的新链即子链,即 iptables 是通过-j
把子链挂到某个规则下面。比如创建一个允许 SSH 访问的白名单列表,可以创建一个新的子链,SSH 相关的策略都放在这个新链中:
以上第二条命令表示将所有访问本机端口 22 的包都放到SSH_Access_List
这条子链上处理,然后这条子链上添加了许多白名单规则,由于进到这个子链的一定是目标 22 端口的,因此规则无需要在指定--dport
参数,最后一个DROP
表示不在白名单列表中的包直接丢掉。
需要注意的是白名单规则中的动作不是ACCEPT
而是RETURN
,这两者有什么区别呢?ACCEPT
表示允许包直接通过 INPUT,不需要再匹配 INPUT 的其他规则。而RETURN
则表示只是不需要再匹配该子链下的后面规则,但需要返回到该子链的母链的规则或者子链继续匹配,能不能通过 INPUT 关卡取决于后面的规则。
另外需要注意的是,前面提到内置的 5 条链可以配置 policy,当所有规则都不匹配时,使用 policy 对包进行处置。但是,自定义链是不支持 policy 的,更确切的说,不支持设置 policy,因为自定义链的 policy 只能是RETURN
,即如果子链的规则都不匹配,则一定会返回到母链中继续匹配。
1.5 iptables 总结
本小节简单介绍了 iptables 的功能和用法,总结如下:
iptables 通过规则匹配决定包的去向,规则由匹配条件+动作构成,规则通过
-I
、-A
插入。五链五表,五链为
PREROUTING
、INPUT
、FORWARD
、OUTPUT
、POSTROUTING
,五表为raw
、mangle
、nat
、filter
、security
。链、表、规则都是有顺序的。当链中的所有规则都不匹配时,iptables 会根据链设置的默认策略 policy 处理包,通过 policy 设置为
ACCEPT
,不建议配置为DROP
。可以创建子链挂在内置链中,子链的 policy 为
RETURN
,不支持配置。匹配条件包括基本匹配条件以及扩展模块提供的扩展匹配条件,扩展匹配条件通过
-m
参数加载,需要记住的扩展模块为comment
、tcp
、udp
、icmp
、mac
、state
、physdev
、set
。常见的 iptables 动作(target)为
ACCEPT
、DROP
、RETURN
、LOG
以及跳转到子链。
2 OpenStack 安全组简介
2.1 Neutron 安全组 VS Nova 安全组?
OpenStack 安全组最开始是通过 Nova 管理及配置的,引入 Neutron 后,新 OpenStack 安全组则是通过 Neutron 管理,并且关联的对象也不是虚拟机,而是 port。我们在页面上把虚拟机加到某个安全组,其实是把虚拟机的 port 关联到安全组中。
由于历史的原因,可能还有些版本的 Nova 依然保留着对安全组规则的操作 API,不过不建议使用,建议通过 Neutron 进行安全组规则管理。
2.2 security group VS firewall
很多刚开始接触 OpenStack 的用户分不清楚安全组(security group)和防火墙(firewall)的区别,因为二者都是做网络访问控制的,并且社区都是基于 iptables 实现的。其实二者的区别还是比较大的,
security group 主要是做主机防护的,换句话说安全组是和虚拟机的 port 相关联,安全组是针对每一个 port 做网络访问控制,所以它更像是一个主机防火墙。而 firewall 是针对一个 VPC 网络的,它针对的是整个 VPC 的网络控制,通常是在路由做策略。因此 security group 在计算节点的 tap 设备上做,而 firewall 在网络节点的 router 上做。
相对于传统网络模型,security group 其实就是类似于操作系统内部自己配置的防火墙,而 firewall 则是旁挂在路由器用于控制整个局域网网络流量的防火墙。
security group 定义的是允许通过的规则集合,即规则的动作就是 ACCEPT。换句话说定义的是白名单规则,因此如果虚拟机关联的是一个空规则安全组,则虚拟机既出不去也进不来。并且由于都是白名单规则,因此安全组规则顺序是无所谓的,而且一个虚拟机 port 可以同时关联多个安全组,此时相当于规则集合的并集。而 firewall 规则是有动作的(allow,deny,reject),由于规则既可以是 ACCEPT,也可以是 DROP,因此先后顺序则非常重要,一个包的命运,不仅取决于规则,还取决于规则的优先级顺序。
前面说了 security group 针对的是虚拟机 port,因为虚拟机的 IP 是已知条件,定义规则时不需要指定虚拟机 IP,比如定义入访规则时,只需要定义源 IP、目标端口、协议,不需要定义目标 IP。而防火墙针对的是整个二层网络,一个二层网络肯定会有很多虚拟机,因此规则需要同时定义源 IP、源端口、目标 IP、目标端口、协议。之前有人问我一个问题,多个虚拟机关联到了一个安全组,想针对这几个虚拟机做网络访问控制,源 IP 是 192.168.4.5,但我只想开通到两个虚拟机的 80 端口访问,问我怎么做?我说实现不了,因为关联在同一个安全组的虚拟机网络访问策略是必须是一样的,你没法指定目标 IP,如果虚拟机有不同的访问需求,只能通过关联不同的安全组实现。
security group 通常用于实现东西向流量控制实现微分段策略,而 firewall 则通常用于实现南北向流量控制。
2.3 安全组用法介绍
前面介绍了安全组,安全组其实就是一个集合,需要把安全组规则放到这个集合才有意义。
Neutron 通过security-group-create
子命令创建安全组,参数只有一个name
,即安全组名称:
不过 Neutron 创建的新安全组并不是一个空规则安全组,而是会自动添加两条默认规则:
即禁止所有的流量访问,允许所有的流量出去。
创建了安全组后,就可以往安全组里面加规则了。Neutron 通过security-group-rule-create
子命令创建,涉及的参数如下:
--direction
: 该规则是出访(egress)还是入访(ingress)。--ethertype
: 以太网类型,ipv4 或者 ipv6。--protocol
: 协议类型,tcp/udp/icmp 等。不指定该参数则表示任意协议。--port-range-min
、--port-range-max
端口范围,如果只有一个端口,则两个参数填一样即可,端口范围为 1~65535。--remote-ip-prefix
,如果是入访则指的是源 IP 地址段,如果是出访则指的是目标 IP 段,通过 CIDR 格式定义,如果只指定一个 IP,通过x.x.x.x/32
指定,如果是任意 IP,则通过0.0.0.0/0
指定。--remote-group-id
: 除了通过 ip 段指定规则,OpenStack 还支持通过安全组作为匹配条件,比如允许关联了 xyz 安全组的所有虚拟机访问 22 端口。
创建一条安全组规则只允许 192.168.4.5 访问虚拟机 SSH 22 端口:
需要注意的是创建安全组和安全组规则只是一个逻辑操作,并不会创建任何 iptables 规则,只有当安全组被关联到 port 时才会真正创建对应的 iptables 规则。
关联安全组通过 Neutron 的port-update
命令,比如要把虚拟机 uuid 为38147993-08f3-4798-a9ab-380805776a40
添加到该安全组:
安全组命令操作参数较多,相对复杂,可以通过 Dashboard 图形界面操作,如图:
具体操作这里不多介绍。
3 安全组实现原理分析
3.1 虚拟机网络流向路径
Linux 网络虚拟化支持 linux bridge 以及 openvswitch(简称 OVS),OpenStack Neutron ml2 驱动二者都支持,目前大多数使用的是 OVS。
不过早期的 iptables 不支持 OVS bridge 以及 port,因此为了实现安全组,虚拟机的 tap 设备并不是直接连接到 OVS bridge 上,而是中间加了一个 Linux bridge,通过 veth pair 连接 Linux bridge 以及 OVS bridege,这样就可以在 Linux bridge 上添加 iptables 规则实现安全组功能了。
目前大多数的 OpenStack 环境还遵循如上规则,简化的虚拟机流量路径如下:
其中 X、Y、Z 为虚拟机 port UUID 前 11 位。
3.2 安全组规则挂在 iptables 哪条链?
根据前面的基础,不难猜出安全组的 iptables 规则肯定是在 filter 表实现的,filter 表只涉及 INPUT、FORWARD、OUTPUT 三条链,iptables 规则流向图可以简化为:
做过主机防火墙的可能第一直觉会认为安全组规则会挂在 INPUT 以及 OUTPUT 链上,但根据上面的流程图,当包不是发往自己的,根本到不了 INPUT 以及 OUTPUT,因此显然在 INPUT、OUTPUT 根本实现不了安全组规则,因此安全组的 iptables 规则肯定是在 FORWARD 链上实现的,也就是说计算节点不处理虚拟机的包(发给自己的包除外),只负责转发包。
3.3 安全组规则定义
为了便于后面的测试,我提前创建了一台虚拟机int32bit-server-1
,IP 为 192.168.100.10/24,port UUID 为3b90700f-1b33-4495-9d64-b41d7dceebd5
,并添加到了之前创建的int32bit-test-secgroup-1
安全组。
我们先导出本计算节点的所有 tap 设备对应 Neutron 的 port,该脚本在 githubint32bit/OpenStack_Scripts可以下载:
根据前面的分析,虚拟机安全组是定义在 filter 表的 FORWARD 链上的,我们查看该链的规则:
FORWARD 链先跳到neutron-filter-top
子链上,neutron-filter-top
链会又跳到neutron-openvswi-local
,而neutron-openvswi-local
链是空链,因此会返回到母链 FORWARD 上,因此这里第一条规则其实没啥用。
返回到 FORWARD 链后继续匹配第 2 条规则,跳转到了neutron-openvswi-FORWARD
,我们查看该链的规则:
该链上一共有 4 条规则,第 1、2 台规则对应的 tap 设备分别为 dhcp 以及 router_interface 端口,即允许 DHCP 以及网关的 port 通过。
而tap3b90700f-1b
显然是虚拟机 port 对应的 tap 设备(名称为 tap+portUUID 前 11 位),第 3、4 规则表明无论是从这个 tap 设备进的还是出的包都进入子链neutron-openvswi-sg-chain
处理。
我们继续查看neutron-openvswi-sg-chain
查看链:
从规则我们可以看出:
--physdev-out
表示从 tap3b90700f-1b 出来发往虚拟机的包,通过子链neutron-openvswi-i3b90700f-1
处理,即虚拟机入访流量。--physdev-in
表示从虚拟机发出进入 tap3b90700f-1b 的包,通过子链neutron-openvswi-o3b90700f-1
处理,即虚拟机出访流量。
显然neutron-openvswi-i3b90700f-1
和neutron-openvswi-o3b90700f-1
分别对应安全组的入访规则和出访规则,即虚拟机的入访规则链为neutron-openvswi-i + port前缀
,虚拟机的出访规则链为neutron-openvswi-i + port前缀
。
3.4 安全组入访规则
由 3.3 我们了解到,安全组入访规则链为neutron-openvswi-i3b90700f-1
,我们查看该链规则:
一共有 6 条规则:
第 1 条规则我们在前面已经介绍过,应该很熟悉了,主要用于放行回包。
第 2、3 条规则主要用于放行 dhcp 广播包。
第 4 条即我们前面添加的安全组规则。
第 5 条规则丢弃无用包。
第 6 条用来处理所有规则都不匹配的包,跳转到
neutron-openvswi-sg-fallback
链,而该链其实只有一条规则,即 DROP ALL。因此不匹配安全组规则的包都会直接丢弃。
安全组入访规则中第 1、2、3、5、6 都是固定的,当有新的安全组策略时就往第 4 条规则后面追加。
3.5 安全组出访规则
由 3.3 我们了解到,安全组入访规则链为neutron-openvswi-o3b90700f-1
,我们查看该链规则:
一共有 8 条规则:
第 1、3 条规则用于放行虚拟机 DHCP client 广播包。
第 2 条规则,放到第 4 章再介绍。
第 4 条规则用于阻止 DHCP 欺骗,避免用户在虚拟机内部自己启一个 DHCP Server 影响 Neutron 的 DHCP Server。
第 5 条规则不再解释。
第 6 条规则是我们的安全组规则,因为我们的安全组出访是 ANY,因此所有包都放行。
第 7 条规则丢弃无用包。
第 8 条规则用来处理所有规则都不匹配的包,跳转到
neutron-openvswi-sg-fallback
链,而该链其实只有一条规则,即 DROP ALL。因此不匹配安全组规则的包都会直接丢弃。
3.6 安全组使用安全组作为匹配条件
前面 2.3 节提到,安全组不仅支持通过 IP 地址段作为源或者目标的匹配条件,还支持通过指定另一个安全组,这种情况怎么处理呢。
为了测试我把创建了一个新的安全组 int32bit-test-secgroup-2 以及新的虚拟机 int32bit-server-2(192.168.100.7),并且 int32bit-server-2 关联了安全组 int32bit-test-secgroup-2。
同时在 int32bit-test-secgroup-1 上增加一条入访规则,允许关联 int32bit-test-secgroup-2 的虚拟机访问 8080 端口:
我们查看虚拟机入访规则链neutron-openvswi-i3b90700f-1
:
我们发现插入了一条新的规则,编号为 4。该规则使用了set
扩展模块,前面介绍过set
是用来匹配 ipset 的,后面的参数NIPv4fc83d82a-5b5d-4c90-80b0-
为 ipset 名,显然是由NIPv4+安全组UUID前缀
组成。
我们查看该 ipset:
可见 192.168.100.7 在 ipset 集合中。
因此 OpenStack 安全组使用安全组作为匹配条件时是通过 ipset 实现的,每个安全组会对应创建一个 ipset 集合,关联的虚拟机 IP 会放到这个集合中,iptables 通过 ipset 匹配实现了安全组匹配功能。
4 安全组 anti snoop 功能
前面 3.5 节提到第 2 条规则,所有的包都会先进入neutron-openvswi-s3b90700f-1
子链处理,这个链是干什么的呢?
我们首先查看下里面的规则:
这条链的处理逻辑很简单,只放行 IP 是 192.168.100.10 并且 MAC 地址是 FA:16:3E:A0:59:BA 的包通过。这其实是 Neutron 默认开启的反欺骗 anti snoop 功能,只有 IP 和 MAC 地址匹配 Neutron port 分配的才能通过。换句话说,你起了个虚拟机 IP 为 192.168.3.1,然后自己手动把网卡的 IP 篡改为 192.168.3.2,肯定是不允许通过的。
但是呢,我们业务又往往有 virtual ip 的需求,最常见的如 haproxy、pacemaker 的 vip。OpenStack 考虑了这种需求,支持用户添加白名单列表,通过 port 的 allowed address pairs 配置。
比如我有两个虚拟机,IP 分别为 192.168.0.10、192.168.0.11,申请了一个 port 192.168.0.100 作为这个两个虚拟机的 vip,可以通过 Neutron 更新 port 信息实现:
添加后我们再查看下neutron-openvswi-s3b90700f-1
链规则:
可见在最前面添加了一条规则允许 IP 为 192.168.0.100 的包通过,此时在虚拟机 192.168.0.10 上把 IP 改为 192.168.0.100 也可以 ping 通了。
5 虚拟机访问宿主机怎么办?
我们已经知道,安全组是在 filter 表的 FORWARD 链上实现的,但如果虚拟机的包是去往宿主机时,由于内核判断目标地址就是自己,因此不会流到 FORWARD 链而是发往 INPUT 链,那这样岂不就是绕过安全组规则了吗?
解决办法很简单,只需要把neutron-openvswi-o3b90700f-1
再挂到 INPUT 链就可以了。
我们查看 INPUT 链规则:
即:
有人可能会问,那宿主机发往虚拟机的包会出现问题吗?需要在 OUTPUT 链上添加规则吗?答案是不需要,因为从 OUTPUT 直接出去,当作正常流程走就可以了。
6 总结
本文首先简单介绍了下 iptables,然后介绍 OpenStack 安全组,最后详细分析了安全组的实现原理。
另外写了一个脚本可以快速导出虚拟机的 iptables 规则,需要在计算节点上运行:
评论 1 条评论