云计算时代容器安全综述-容器的网络安全(下)

从上这篇文章中我们知道IP地址是网络通信中非常重要的信息,因此对于容器实例来说,为了被访问到或者访问其他服务,要么有自己独立的IP地址,要么和宿主机共享相同的IP地址。特别是对于前者,由于容器实例有自己独享的网络命名空间,因此容器实例有独享的网络栈,端口号就不会和宿主机冲突,隔离性得到了极大的提升。从原理上看,Docker提供了完整的容器网络方案,但是Kubernetes在Docker的基础上进行了演进,因此咱们为了切合实际场景,直接先看看在Kubernetes中,IP地址是如何管理和分配的。

在Kubernetes平台上,每个POD都会分配独立的IP地址,并且如果POD中包含了多个容器应用,那么同一个POD中的所有容器实例共享POD的IP地址,背后的原理也不难理解,读过笔者关于Kubernetes系列文章的同学,应该能脱口而出:因为运行在相同POD中的多个容器实例共享相同的网络命名空间。作为集群的运维管理人员,我们在讲某个Node(主机,宿主机)加入到集群的时候,会配置Node可以使用的IP地址范围,专业的术语叫CIDR地址块,那么当调度器(Sheduler)基于自己的调度算法将某个POD调度到主机之后,POD会从分配给Node的这个CIDR地址块获取到一个可用的IP地址。

注:笔者上边的描述进行了简化和抽象,实际上在有些托管环境中,可能并总是给Node分配一块连续的IP地址供使用(CIDR),比如在AWS中,IP地址分配是通过一个插架的方式提供。

另外在Kubernetes平台上,集群中的POD之间可直接进行通信而不需要做依赖于NAT机制(Network Address Translation),用大白话说NAT就是:目标IP地址可以是某个固定的IP地址,即便是提供服务的POD持有的IP地址和这个固定IP地址之间存在映射关系。另外在Kubernetes中,我们还可以通过网络策略(Network policies)机制来约束POD之间的可访问性。

说到Kubernertes中的POD服务访问的问题,就不能不提Service对象,本质上来说,Service对象就是一种NAT的具体实现,Service对象是Kuberntes提供的对象资源,有自己独享的IP地址,从这个角度看,Service也就仅仅是一个IP地址而已。因为Service对象没有具体的接口,也没有监听的概念,并且Service的IP地址主要被用来做服务调用的负载均衡之用。具体来说Service对象身后有多个POD提供具体的服务,因此当我们的访问请求到达Service对象的IP地址后,需要转发给身后的某个POD来进行实际的处理,我们把这种基于Layer 3的负载均衡叫做3层路由,咱们稍后会详细介绍实现原理。

在传统的组网方案中,我们一般通虚拟局域网(vlan)来对不同业务目标的系统进行安全隔离,在容器化部署的场景下,特别是在Docker平台上,我们可以使用docker network来进行网络设置,但是Kuberntes的出现为我们提出了更多的挑战,因为默认情况下,在同集群内,POD之间默认互通。这种默认互通也包含了管控节点(master node),因为control components(API Server,ETCD,调度器,Controller等)和运行业务负载的pod都在同一个网络中,如果读者有电信的背景,会觉得这简直是开历史倒车,因为电信系统中,组网的核心就是将不同级别的设备和组件划分到不同的网络中,这主要是解决安全问题。

那么在Kubernetes中,如何给这个大虚拟机局域网划分不同的区域呢?Kubernetes给出的方案就是网络策略(network pollicies)。不过在介绍网络策略之前,咱们先来详细的介绍一下3层负载均衡的实现机制。从网络分层的角度看(不是很清楚的同学建议先阅读本篇文章的“上”),3层解决的主要问题就是路由的问题,也就是数据包下一跳的问题,而决定数据包下一跳的核心是路由表,简单说路由表中提供了到某个地址应该发送数据到哪个IP地址以及走哪个网络接口(网卡)的信息。转发数据包(路由)只是3层提供的多项能力的之一,我们还可以进行drop数据包,负载均衡,NAT(修改IP地址),验证防火墙规则,验证网络安全策略等工作。

除了在网络协议栈的3层可以对数据进行这些个丰富的操作,在4层(传输层)我们也可以基于端口做很多相同的工作,这些规则验证重度依赖于内核提供的一种叫netfiler的功能。具体来说netfiler是Linux内核版本2.4引入的一种数据包过滤框架(packet-filtering framework),netfiler的工作原理是基于一组作用于源IP地址和目标地址的”规则“来决定数据包(packet)的下一步动作。通过上边的描述大家应该可以体会到这套机制的核心是”规则“,这些规则是我们用来实现特定业务场景的基础,而在Kubernetes平台中,这个规则通过iptables和IPVS来配置。

iptables提供了多种不同的table类型来配置数据包处理的规则,通过iptables配置的规则,会被kernel中的netfiler使用并apply到符合条件的数据包处理链路中。具体来说,在容器网络的上下文中,有两种类型的table类型需要大家深入的了解:

- filter,用来决定接到到的数据包具体是drop还是forward

- nat,对接收到的数据包进行网络地址翻译(Network address translation)

我们可以在全新安装的ubuntu操作系统上,以root账户运行iptables -L,笔者机器上命令返回的信息如下:

root@6ce15134fd1a:/# iptables -L

Chain INPUT (policy ACCEPT)

target    prot opt source              destination

Chain FORWARD (policy ACCEPT)

target    prot opt source              destination

Chain OUTPUT (policy ACCEPT)

target    prot opt source              destination

从输出的结果可以看到多个chain,关于如何操作iptables咱们后边详细介绍,大家也有个感觉就行。通过iptables提供的配置netfiler规则的能力,我们可以实现很多安全目标。比如容器防火墙技术方案,Kubernetes的网络插件等都重依赖于iptables来实现丰富的网络安全策略。特别是在Kubernetes平台中,kube-proxy实用iptables规则来实现service到多个后端pod的负载均衡。具体来说,在Kubernetes中,每个service都有内部DNS名称,DNS名称会被翻译成具体的IP地址,当请求发送到service对应的ip地址后,通过定义在iptables规则,通过DNAT机制来讲目标地址替换为service身后某个pod的ip地址,然后请求就发给具体的POD来进行后续的处理。当支持服务的pod发生变化,比如删除了一个pod并增加了一个信息的,iptables会进行相应的更新来体现最新的映射关系。

在Kubernetes环境中,我们可以通过kubectl客户端工具很容易看到service服务和POD以及iptables之间的关系,假设咱们有一个my-nginx的服务,服务有两个pod,我们可以通过iptables -t nat -L来过滤只看和nat相关的table,在有较多业务应用部署的机器上运行这个命令会返回很多信息,但是也不难从这些信息中找到我们感兴趣的规则。因为我们的服务的ip地址为10.100.132.10,你可以通过这个IP地址找到chain KUBE-SERVICES,如下所示:

Chain KUBE-SERVICES (2 references)

target prot opt source destination

...

KUBE-SVC-SV7AMNAGZFKZEMQ4 tcp -- anywhere 10.100.132.10 /* default/mynginx:

http cluster IP */ tcp dpt:http-alt

...

虽然说这个rule的规则并不是大白话,但是咱们这些悟性极高的科技行业从事很容易从中找到这叫KUBE-SVC-SV7AMNAGZFKZEMQ4目标chain,我们继续往下翻就可以找到这个KUBE-SVC-SV7AMNAGZFKZEMQ4的详细定义:

Chain KUBE-SVC-SV7AMNAGZFKZEMQ4 (2 references)

target prot opt source destination

KUBE-SEP-XZGVVMRRSKK6PWWN all -- anywhere anywhere statistic mode

random probability 0.50000000000

KUBE-SEP-PUXUHBP3DTPPX72C all -- anywhere anywhere

眼尖的同学通过probability 0.5这样的配置应该能看出来这是负载均衡,因为请求到了这chain之后,会按照50%的几率来平均分配到两台POD,分别是KUBE-SEP-XZGVVMRRSKK6PWWN和KUBE-SEP-PUXUHBP3DTPPX72C。考虑周全的同学可能会问,如果是3个POD,能除的尽吗?笔者建议大家在自己的环境中通过kubectl scale来调整POD的数量。

那么上边这个chain中的两个以KUBE-SEP的chain到底是啥,相信你也能猜出来这是具体的POD信息,咱们继续往下翻就能找到这两个chain的定义:

Chain KUBE-SEP-XZGVVMRRSKK6PWWN (1 references)

target        prot opt source destination

KUBE-MARK-MASQ all -- 10.32.0.3 anywhere

DNAT tcp -- anywhere anywhere tcp to:10.32.0.3:80

...

Chain KUBE-SEP-PUXUHBP3DTPPX72C (1 references)

target        prot opt source destination

KUBE-MARK-MASQ all -- 10.32.0.4 anywhere

DNAT tcp -- anywhere anywhere tcp to:10.32.0.4:80

如果读者那上边的输出信息来对比kubectl get svc,pods -o wide输出的信息,会发现完全一致,这也符合预期。不过iptables有个很大的问题,如果机器上有很多应用和pod,iptables的性能会变得很差,笔者看到过一个计算,在有2000个服务以及每个服务有10个pod的机器上,会产生大概20000个iptables规则。因此Kubernetes现在主流使用的是IPVS技术,咱们继续IPVS这个话题。

IPVS又称IP Virtual Server,通常被称作Layer 4负载均衡或者Layer 4 Lan switching技术。IPVS和iptables类似,是另外一种规则的实现机制,和iptables最大的不同就是使用hash表来优化负载均衡机制。

可以说IPVS是为kube-proxy量身定做,因为我们在Kubenernetes环境下需要依赖于kube-proxy来访问Node上运行的容器实例提供的服务。虽然说从技术方案上看,IPVS的性能要比iptables好很多,但是我们并不能直接得出这样的结论,iptables最近几年也设计了增量更新的机制,因此关于iptables和IPVS之争,我们拭目以待。

至于说我们在项目上到底使用哪种方式来定制化netfiler的规则,这个本质上不是咱们应用系统开发人员关心的,不过有这部分知识能够更好的帮助我们进行风险评估以及故障根因分析。由于规则的执行在内核里,并且内核在多个容器实例之间共享,因此无论在什么情况下操作netfiler规则,大家要考虑清楚爆炸半径。

咱们将视线拉回到Kubernetes的场景下,Kubernetes通过网络策略(network policies)来控制POD之间的数据流动规则。策略通常由port,ip地址,服务,以及labeled pods组成,当发送或者收到数据消息,如果消息无法通过策略,那么就拒绝或者drop,比如如下所示的NetworkPolicy对象yaml文件定义,定义了my-nginx服务的ingress访问规则:

《图1.1 NetworkPolicy对象YAML定义》

接下来我们在自己的集群中通过kubectl apply来部署这个网络规则后,通过iptables -L就能看到具体增加的规则:

Chain WEAVE-NPC-INGRESS (1 references)

target prot opt source destination

ACCEPT all -- anywhere anywhere match-set weave-{U;]TI.l|Md

RzDhN7$NRn[t)d src match-set weave-vC070kAfB$if8}PFMX{V9Mv2m dst /* pods: namespace: default, selector: access=true -> pods: namespace: default, selector: app=mynginx (ingress) */

关于谁更新了宿主机上的iptables这个问题,大家一定要记住,是网络插件而不是Kubernetes自身负责更新iptables。从上边的输出大家大概也能猜测出来,笔者的环境使用的网络插件weave。

了解完Kubernetes的工作原理之后,咱们接下来通过直接操作iptables来体验一下iptables的强大,在新创建的ubuntu机器上运行命令:while true; do echo "hello world" | nc -l 9000 -N; done 来启动一个在9000端口运行的服务。

然后在另外一个窗口上我们可以通过curl localhost:9000来验证服务可访问性。然后我们运行命令sudo iptables -I INPUT -j REJECT -p tcp --dport=9000 来增加iptable规则,然后运行iptables -L观察更新后的iptables,如下图所示:

《图1.2 iptables更新后的返回结果》

然后我们可以重新尝试curl本地9000端口上的服务,输出如下:

# curl localhost:9000

curl: (7) Failed to connect to localhost port 9000: Connection refused

从数据的结果可以看到,我们手动设定规则生效了。从上边这个简单的例子我们可以看到如何使用iptables来约束服务的流量,不过笔者不建议大家通过手动的方式来管理容器实例或者POD的流量,因为这总工作已经有非常成熟的工具支持。

Kubernetes的NetworkPolicy机制为我们提供了一种更加容易的方式来使用iptables的规则,关于如何使用Network policy大家可以参考相关的文档或者官方文档,笔者结合过往的实践经验,有如下的最佳实践总结:

- Default Deny,默认情况下denies所有的ingress访问流量,通过网络策略之开放给特定客户端,这也是最小权限安全规范的体现

- Default Deny egress,和ingress流量类似,默认情况下也拒绝所有的对外访问流量,通过policies来对特定访问场景开发

- 通过polices限制pod和pod之间的访问流量

- 通过polices限制port端口的使用,让应用只有特定端口才能对外提供服务,这其实和我们传统上配置服务器遵守的安全策略类似,缩小攻击面

到目前为止本篇文章讨论都是3层4层的负载均衡机制,接下来我们来看看4层之上的情况,也就是service mesh(服务网格)机制。服务网格为我们提供了一种全新的解决应用程序相互之间连接的思路。业界主流的服务网格方案包括但不限于:Istio,Envoy,Linkerd以及阿里云提供的托管服务网格服务。

服务网格需要依赖于Kubernetes提供的环境运行,代理容器会以边车容器的形式被注入到POD中,这样所有流入和流出POD的流量都会通过边车代理,那么我们就可以在边车代理上进行安全控制,实施安全策略等。笔者要特别强调的是,服务网格提供的应用程序层的安全手段和我们在3,4层介绍的安全机制不冲突,这也是纵深防御思路的体现。

从生命周期的角度看,边车容器的生命周期和应用程序容器实例的生命周期一致,如果应用程序容器实例被攻破,那么攻击者可能会绕过边车容器来访问策略不允许的资源。特别是边车容器和应用容器通常共享网络命名空间,因此我们必须反复确认应用程序容器启动的时候没有被赋予CAP_NET_ADMIN权限,这样即便是应用程序容器被攻破,也无法修改宿主机上的iptables,造成应用程序宕机。

注:笔者前边的例子中,在docker中启动ubuntu实例的时候,需要主动赋予CAP_NET_ADMIN权限,要不然无法在启动的容器实例中查看为这个容器新建命名空间中网络协议栈的iptables信息,具体的命名为:docker run -i -t --name qigaopan --cap-add NET_ADMIN ubuntu bash,读者注意cap-add参数。

好了,今天这篇文章就这么多了,咱们下篇文章继续讨论连接安全,或者简单说就是HTTPS,以及背后的SSL技术原理,敬请期待!

你可能感兴趣的:(云计算时代容器安全综述-容器的网络安全(下))