Molet

深入浅出解析 OpenStack 安全组实现原理

Molet 安全防护 2023-01-23 805浏览 0

深入浅出解析 OpenStack 安全组实现原理

1、iptables简介

1.1 iptables概述

在介绍OpenStack安全组前先简单介绍下iptables,其实iptables只是一个用户空间的程序,真正干活的其实是Linux内核netfilter,通过iptables创建新规则,其实就是在netfilter中插入一个hook,从而实现修改数据包、控制数据包流向等,对iptables使用方法不熟悉的可以参考图文并茂理解iptables[1].

简单地说,iptables就是通过一系列规则条件匹配执行指定的动作,因此一条规则就是由条件+动作构成,条件比如源IP地址、四层协议、端口等,动作如拒绝、通过、丢弃、修改包等,动作通常通过-j参数指定。

比如拒绝192.168.1.2访问目标22端口,只需要添加如下iptables规则:

深入浅出解析 OpenStack 安全组实现原理

如上:

  • -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通过,通过如下规则配置:

深入浅出解析 OpenStack 安全组实现原理

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 bridge br0,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]:

深入浅出解析 OpenStack 安全组实现原理

(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表各个链的默认策略:

深入浅出解析 OpenStack 安全组实现原理

如果一条链规则特别多且复杂,管理起来非常麻烦,因此很有必要对链根据功能分组。iptables通过自定义链实现。用户可以通过iptables -N name创建一个新链,然后和内置链一样可以往新链中添加规则。但是需要注意的是,自定义链不能独立存在,必须挂在内置5条链下面,即必须是内置链的子链。

前面1.3节提了下-j可以指定一条新链,这里的新链即子链,即iptables是通过-j把子链挂到某个规则下面。比如创建一个允许SSH访问的白名单列表,可以创建一个新的子链,SSH相关的策略都放在这个新链中:

深入浅出解析 OpenStack 安全组实现原理

以上第二条命令表示将所有访问本机端口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的功能和用法,总结如下:

1、iptables通过规则匹配决定包的去向,规则由匹配条件+动作构成,规则通过-I、-A插入。

2、五链五表,五链为PREROUTING、INPUT、FORWARD、OUTPUT、POSTROUTING,五表为raw、mangle、nat、filter、security。链、表、规则都是有顺序的。

3、当链中的所有规则都不匹配时,iptables会根据链设置的默认策略policy处理包,通过policy设置为ACCEPT,不建议配置为DROP。

4、可以创建子链挂在内置链中,子链的policy为RETURN,不支持配置。

5、匹配条件包括基本匹配条件以及扩展模块提供的扩展匹配条件,扩展匹配条件通过-m参数加载,需要记住的扩展模块为comment、tcp、udp、icmp、mac、state、physdev、set。

6、常见的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,即安全组名称:

深入浅出解析 OpenStack 安全组实现原理

不过Neutron创建的新安全组并不是一个空规则安全组,而是会自动添加两条默认规则:

深入浅出解析 OpenStack 安全组实现原理

即禁止所有的流量访问,允许所有的流量出去。

创建了安全组后,就可以往安全组里面加规则了。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端口:

深入浅出解析 OpenStack 安全组实现原理

需要注意的是创建安全组和安全组规则只是一个逻辑操作,并不会创建任何iptables规则,只有当安全组被关联到port时才会真正创建对应的iptables规则。

关联安全组通过Neutron的port-update命令,比如要把虚拟机uuid为38147993-08f3-4798-a9ab-380805776a40添加到该安全组:

深入浅出解析 OpenStack 安全组实现原理

安全组命令操作参数较多,相对复杂,可以通过Dashboard图形界面操作,如图:

深入浅出解析 OpenStack 安全组实现原理

具体操作这里不多介绍。

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环境还遵循如上规则,简化的虚拟机流量路径如下:

深入浅出解析 OpenStack 安全组实现原理

其中X、Y、Z为虚拟机port UUID前11位。

3.2 安全组规则挂在iptables哪条链?

根据前面的基础,不难猜出安全组的iptables规则肯定是在filter表实现的,filter表只涉及INPUT、FORWARD、OUTPUT三条链,iptables规则流向图可以简化为:

深入浅出解析 OpenStack 安全组实现原理

做过主机防火墙的可能第一直觉会认为安全组规则会挂在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,该脚本在github int32bit/OpenStack_Scripts可以下载:

深入浅出解析 OpenStack 安全组实现原理

根据前面的分析,虚拟机安全组是定义在filter表的FORWARD链上的,我们查看该链的规则:

深入浅出解析 OpenStack 安全组实现原理

FORWARD链先跳到neutron-filter-top子链上,neutron-filter-top链会又跳到neutron-openvswi-local,而neutron-openvswi-local链是空链,因此会返回到母链FORWARD上,因此这里第一条规则其实没啥用。

返回到FORWARD链后继续匹配第2条规则,跳转到了neutron-openvswi-FORWARD,我们查看该链的规则:

深入浅出解析 OpenStack 安全组实现原理

该链上一共有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查看链:

深入浅出解析 OpenStack 安全组实现原理

从规则我们可以看出:

  • –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,我们查看该链规则:

深入浅出解析 OpenStack 安全组实现原理

一共有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,我们查看该链规则:

深入浅出解析 OpenStack 安全组实现原理

一共有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端口:

深入浅出解析 OpenStack 安全组实现原理

我们查看虚拟机入访规则链neutron-openvswi-i3b90700f-1:

深入浅出解析 OpenStack 安全组实现原理

我们发现插入了一条新的规则,编号为4。该规则使用了set扩展模块,前面介绍过set是用来匹配ipset的,后面的参数NIPv4fc83d82a-5b5d-4c90-80b0-为ipset名,显然是由NIPv4+安全组UUID前缀组成。

我们查看该ipset:

深入浅出解析 OpenStack 安全组实现原理

可见192.168.100.7在ipset集合中。

因此OpenStack安全组使用安全组作为匹配条件时是通过ipset实现的,每个安全组会对应创建一个ipset集合,关联的虚拟机IP会放到这个集合中,iptables通过ipset匹配实现了安全组匹配功能。

4、安全组anti snoop功能

前面3.5节提到第2条规则,所有的包都会先进入neutron-openvswi-s3b90700f-1子链处理,这个链是干什么的呢?

我们首先查看下里面的规则:

深入浅出解析 OpenStack 安全组实现原理

这条链的处理逻辑很简单,只放行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信息实现:

深入浅出解析 OpenStack 安全组实现原理

添加后我们再查看下neutron-openvswi-s3b90700f-1链规则:

深入浅出解析 OpenStack 安全组实现原理

可见在最前面添加了一条规则允许IP为192.168.0.100的包通过,此时在虚拟机192.168.0.10上把IP改为192.168.0.100也可以ping通了。

5、虚拟机访问宿主机怎么办?

我们已经知道,安全组是在filter表的FORWARD链上实现的,但如果虚拟机的包是去往宿主机时,由于内核判断目标地址就是自己,因此不会流到FORWARD链而是发往INPUT链,那这样岂不就是绕过安全组规则了吗?

深入浅出解析 OpenStack 安全组实现原理

解决办法很简单,只需要把neutron-openvswi-o3b90700f-1再挂到INPUT链就可以了。

我们查看INPUT链规则:

深入浅出解析 OpenStack 安全组实现原理

即:

深入浅出解析 OpenStack 安全组实现原理

有人可能会问,那宿主机发往虚拟机的包会出现问题吗?需要在OUTPUT链上添加规则吗?答案是不需要,因为从OUTPUT直接出去,当作正常流程走就可以了。

6、总结

本文首先简单介绍了下iptables,然后介绍OpenStack安全组,最后详细分析了安全组的实现原理。

另外写了一个脚本可以快速导出虚拟机的iptables规则,需要在计算节点上运行:

深入浅出解析 OpenStack 安全组实现原理

付广平,任职某银行云技术管理中心,负责云计算相关技术研究。毕业于北京邮电大学,从2013开始从事OpenStack相关工作,参与了OpenStack Nova、Cinder、Oslo等项目社区开发。

继续浏览有关 OpenStack 的文章
发表评论