05-多租户

kubernetes的网络模型,只是关注容器之间网络的“连通”,却不关心容器之间网络的“隔离”。如何实现网络的隔离来满足多租户的需求。

NetworkPolicy

在kubernetes中,网络隔离能力的定义,依靠专门的API对象来描述,NetworkPolicy。它定义的规则其实是白名单

一个完整的NetworkPolicy对象的示例如下:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  podSelector:      # 定义这个NetworkPolicy的限制范围
    matchLabels:    # 在当前Namespace中携带role=db标签的Pod
      role: db      # podSelector: {} 表示当前Namespace下所有的Pod
  policyTypes:
  - Ingress     # 影响流入请求
  - Egress      # 影响流出请求
  ingress:
  - from:       # 允许流入的白名单,并列的三种情况(满足一个即生效):
                # ipBlock,namespaceSelector,podSelector
                # 不用`-`分隔字段时,表示条件需要同时满足
    - ipBlock:
        cidr: 172.17.0.0/16     # 源地址网段
        except:
        - 172.17.1.0/24
    - namespaceSelector:
        matchLabels:
          project: myproject
    - podSelector:
        matchLabels:
          role: frontend
    ports:      # 允许流入的端口
    - protocol: TCP
      port: 6379
  egress:
  - to:         # 允许流出的白名单
    - ipBlock:
        cidr: 10.0.0.0/24   # 目的地址网段
    ports:      # 允许流出的端口
    - protocol: TCP
      port: 5978

kubernetes里的Pod默认都是“允许所有(Accept All)”,即:

  • Pod可以接收来自任何发送方的请求
  • 向任何接收方发送请求

如果要对这个情况做出限制,就必须通过NetworkPolicy对象来指定。

一旦pod被NetworkPolicy选中,那么就会进入“拒绝所有”(Deny All)的状态,即:

  • 这个pod不允许被外界访问
  • 也不允许对外界发起访问

上述NetworkPolicy独享指定的隔离规则如下:

  1. 该隔离规则只对default namespace下携带role=db标签的pod有效,限制的请求类型包括ingress和egress
  2. kubernetes会拒绝任何访问被隔离Pod的请求,除非这个请求来自白名单里的对象,并且访问的是被隔离Pod的6379端口
  3. kubernetes会拒绝被隔离Pod对外发起任何请求,除非请求的目的地址属于10.0.0.0/24网段,并且访问的是该网段地址的5978端口

白名单对象包括:

  1. default namespace中携带role=fronted标签的pod
  2. 任何namespace中携带project=myproject标签的pod
  3. 任何源地址属于172.17.0.0/16网段,且不属于172.17.1.0/24网段的请求

要使用上述定义的NetworkPolicy在kubernetes集群中真正产生作用,需要CNI网络插件支持kubernetes的NetworkPolicy。

凡是支持NetworkPolicy的CNI网络插件,都维护着一个NetworkPolicy Controller,通过控制循环的方式对NetworkPolicy对象的增删改查做出响应,然后在宿主机上完成iptables规则的配置工作。

目前实现NetworkPolicy的网络插件包括Calico、Weave和kube-router等。在使用Flannel的同时要使用NetworkPolicy的话,就需要在额外安装一个网络插件,如Calico来负责执行NetworkPolicy。

网络插件对pod进行隔离

以三层网络插件(Calico和kube-router)为例,分析一下实现原理。

创建一个NetworkPolicy对象,如下:

apiVersion: extensions/v1beta1
kind: NetworkPolicy
metadata:
  name: test-network-policy
  namespace: default
spec:
  podSelector:
    matchLabels:
      role: db
  ingress:
   - from:
     - namespaceSelector:
         matchLabels:
           project: myproject
     - podSelector:
         matchLabels:
           role: frontend
     ports:
       - protocol: tcp
         port: 6379

kubernetes网络插件使用上述NetworkPolicy定义,在宿主机上生成iptables规则,具体过程如下:

for dstIP := range 所有被 networkpolicy.spec.podSelector 选中的 Pod  IP 地址 {
  for srcIP := range 所有被 ingress.from.podSelector 选中的 Pod  IP 地址 {
    for port, protocol := range ingress.ports {
      iptables -A KUBE-NWPLCY-CHAIN -s $srcIP -d $dstIP -p $protocol -m $protocol --dport $port -j ACCEPT
      // 规则的名字 KUBE-NWPLCY-CHAIN
      // 含义:当IP包的源地址是srcIP,目的地址是dstIP,协议是protocol,目的端口是port的时候,就运行它通过(ACCEPT)
    }
  }
}

// 通过匹配条件决定下一步iptables规则
// 匹配这条规则所需要的四个参数都是从NetworkPolicy对象里读取出来

kubernetes网络插件对Pod进行隔离,其实是靠宿主机上生成的NetworkPolicy对应的iptables规则来实现的。在设置好上述“隔离”规则之后,网络插件还需要想办法,将所有对被隔离Pod的访问请求,都转发到上述KUBE-NWPLCY-CHAIN规则上去进行匹配,如果匹配不通过这个请求应该被“拒绝”。

设置两组iptables规则

第一组规则

负责拦截对被隔离Pod的访问请求,生成这一组规则的伪代码如下:

for pod := range  Node 上的所有 Pod {
    if pod  networkpolicy.spec.podSelector 选中的 {
        iptables -A FORWARD -d $podIP -m physdev --physdev-is-bridged -j KUBE-POD-SPECIFIC-FW-CHAIN
        // FORWARD链拦截一种特殊情况,它对应的是同一台宿主机上的容器之间经过CNI网桥进行通信的流入数据包
        // --physdev-is-bridged表示这个FORWARD链匹配的是,通过本机上的网桥设备,发往目的地址的IP包
        iptables -A FORWARD -d $podIP -j KUBE-POD-SPECIFIC-FW-CHAIN
        // FORWARD链拦截普通情况,容器跨主机通信,流入容器的数据包都是经过路由转发(FORWARD检查点)来的
        // 这些规则最后都跳转到了KUBE-POD-SPECIFIC-FW-CHAIN规则上,这是NetworkPolicy设置的第二组规则
        ...
    }
}
// iptables规则使用了内置链FORWARD

iptables只是一个操作Linux内核Netfilter子系统的“界面”,Netfilter子系统的作用,就是Linux内核里挡在“网卡”和”用户态进程"之间的一道”防火墙“。他们之间的关系如下所示。

image

IP包一进一出的两条路径上,有几个关键的”检查点“,它们正是Netfilter设置”防火墙“的地方。在iptables中,这些检查点被称为链(Chain)。这些检查点对应的iptables规则是按照定义顺序依次进行匹配的。具体工作原理如下图所示。

image

  1. 当一个IP包通过网卡进入主机之后,它就进入了Netfilter定义的流入路径(Input Path)里。在这个路径中,IP包要经过路由表来决定下一步的去向。
  2. 在这次路由之前,Netfilter设置了PREROUTING的检查点。

在Linux内核实现里,所谓”检查点“实际上就是内核网络协议代码里的Hook(比如,在执行路由判断的代码之前,内核会先调用PREROUTING的Hook)。

  1. 经过路由之后,IP包的去向分为两种:
    1. 继续在本机处理
    2. 被转发到其他目的地

继续在本机处理

  1. IP包将继续向上层协议栈流动,在它进入传输层之前,Netfilter会设置INPUT检查点,至此,IP包流入路径(Input Path)结束
  2. IP包通过传输层进入用户空间,交给用户进程处理。
  3. 处理完成后,用户进程通过本机发出返回的IP包,此时,IP包就进入流出路径(Output Path)
  4. IP包首先经过主机的路由表进行路由
  5. 路由结束后,Netfilter设置OUTPUT检查点
  6. 在OUTPUT之后再设置POSTROUTING检查点

被转发到其他目的地

  1. IP包不进入传输层,继续在网络层流动,从而进入转发路径(Forward Path)
  2. 转发路径中,Netfilter会设置FORWARD检查点
  3. 在FORWARD检查点完成后,IP包来到流出路径(Output Path)
  4. 转发的IP包目的地已经确定,不再经过路由,直接到达POSTROUTING检查点

POSTROUTING的作用,是上述两条路径,最终汇聚在一起的”最终检查点”。

在网桥参与的情况下,上述Netfilter设置检查点的流程,实际上也会出现在链路层(二层),并且会跟上面的网络层(三层)的流程有交互。链路层的检查点对应的操作界面叫作ebtables。数据包在Linux Netfilter子系统里完整的流动过测井如下图所示。

image

上述过程是途中绿色部分,即网络层的iptables链的工作流程。

第二组规则

KUBE-POD-SPECIFIC-FW-CHAIN,做出允许或拒绝的判断,这部分功能的实现,类似如下的iptables规则:

iptables -A KUBE-POD-SPECIFIC-FW-CHAIN -j KUBE-NWPLCY-CHAIN
# 把IP包转交给前面定义的KUBE-NEPLCY-CHAIN规则去进行匹配,匹配成功运行通过,匹配失败进入下一条规则
iptables -A KUBE-POD-SPECIFIC-FW-CHAIN -j REJECT --reject-with icmp-port-unreachable
# REJECT规则,通过这条规则,不满足NetworkPolicy定义的请求会被拒绝掉,从而实现了该容器的“隔离”

总结

NetworkPolicy只是宿主机上一系列iptables规则,与传统IaaS的安全组类似。

kubernetes的网络模型以及大多数容器网络实现,即不会保证容器之间二层网络的互通,也不会实现容器之间二层网络的隔离,这与IaaS项目管理的虚拟机是完全不同的。kubernetes从底层的设计和实现上,更倾向于假设你已经有一套完整的物理基础设施,kubernetes负责在此基础上提供“弱多租户”的能力

上次修改: 14 April 2020