陈计节,企业应用云原生架构师,在腾讯企业 IT 负责云原生应用治理产品的设计与研发工作,主要研究利用容器集群和服务网格等云原生实践模式降低微服务开发与治理门槛并提升运营效率。
给需要快速解决问题的集群管理员:
在 TKE Stack 中正确安装 Istio CNI 有两种方式:如果你的 TKE Stack 集群所使用 Galaxy 版本可以支持 cniVersion 0.3.1,请以默认的方式安装 Istio CNI;否则请使用以“网卡插件”的方式安装 Istio CNI,并在创建 Pod 时指定使用集群默认网络名称。
如果你发现你的 TKE Stack 集群安装完 Istio CNI 之后,无法创建新的 Pod,请立即卸载已安装的 Istio CNI,并手动恢复各个节点上写入的 Galaxy 配置文件:将 /etc/cni/net.d/00-galaxy.conflist 文件内的 plugins 数组字段的第一个元素提取出来,并保存为单独的 conf 文件: /etc/cni/net.d/00-galaxy.conf。删除正在创建中、但无法成功的 Pod,等待其重建,Pod 的创建功能应该能自动恢复。
Istio 是流行的服务网格软件,它通过向业务 Pod 注入可捕获出入口流量的代理软件 Envoy 作为 Sidecar 来完成对流量的观测与治理。
Istio 为了让 Envoy 代理能够捕获来去业务容器的流量,需要向 Pod 所在网络下发如下 IPTABLES 规则:
*nat -N ISTIO_INBOUND -N ISTIO_REDIRECT -N ISTIO_IN_REDIRECT -N ISTIO_OUTPUT -A ISTIO_INBOUND -p tcp --dport 15008 -j RETURN -A ISTIO_REDIRECT -p tcp -j REDIRECT --to-ports 15001 -A ISTIO_IN_REDIRECT -p tcp -j REDIRECT --to-ports 15006 -A PREROUTING -p tcp -j ISTIO_INBOUND -A ISTIO_INBOUND -p tcp --dport 22 -j RETURN -A ISTIO_INBOUND -p tcp --dport 15090 -j RETURN -A ISTIO_INBOUND -p tcp --dport 15021 -j RETURN -A ISTIO_INBOUND -p tcp --dport 15020 -j RETURN -A ISTIO_INBOUND -p tcp -j ISTIO_IN_REDIRECT -A OUTPUT -p tcp -j ISTIO_OUTPUT -A ISTIO_OUTPUT -o lo -s 127.0.0.6/32 -j RETURN -A ISTIO_OUTPUT -o lo ! -d 127.0.0.1/32 -m owner --uid-owner 1337 -j ISTIO_IN_REDIRECT -A ISTIO_OUTPUT -o lo -m owner ! --uid-owner 1337 -j RETURN -A ISTIO_OUTPUT -m owner --uid-owner 1337 -j RETURN -A ISTIO_OUTPUT -o lo ! -d 127.0.0.1/32 -m owner --gid-owner 1337 -j ISTIO_IN_REDIRECT -A ISTIO_OUTPUT -o lo -m owner ! --gid-owner 1337 -j RETURN -A ISTIO_OUTPUT -m owner --gid-owner 1337 -j RETURN -A ISTIO_OUTPUT -d 127.0.0.1/32 -j RETURN -A ISTIO_OUTPUT -j ISTIO_REDIRECT COMMIT
在常规安装模式下,下发 IPTABLES 规则的操作,是通过与 Envoy 代理容器一同注入的初始化容器 istio-init 完成的。向 Pod 网络下发 IPTABLES 规则要求 Pod 可以使用 NET_ADMIN 和 NET_RAW 两个高权限功能(Capabilities)。Linux 将传统与超级用户 root 关联的特权划分为不同的单元,称为 Capabilites。Capabilites 每个单元都可以独立启用和禁用。这样当系统在做权限检查的时候就检查特定的 Capabilites,并决定当前用户其进程是否可以进行相应特权操作。比如如果要设置系统时间,就得具有 CAP_SYS_TIME 这个 Capabilites。
容器本质上是是宿主机上运行的进程,虽然容器运行时默认只向容器提供必要 Capabilities,但如果使用 --privileded
模式运行容器,或者向容器追加更多 Capabilities 时,容器就可以像其他进程一样拥有很高权限的操作能力。这样,能使用 NET_ADMIN 和 NET_RAW 权限的 Pod 理论上不光可以操作自己这个 Pod 的网络,如果处理不当,还可能影响到同一工作节点的其他 Pod 甚至是宿主机的网络配置。通常,这在一些对容器应用的权限严格限制的环境中,是不推荐使用的。
从 Kubernetes 1.11 版本开始,我们可以在集群中使用 PodSecurityPolicy 资源(PSP)来限制集群中的 Pod 可以使用的默认权限或能力。通过编写如下 PSP 即可限制集群内的 Pod 均不得使用任何特权 Capabilities:
apiVersion: policy/v1beta1 kind: PodSecurityPolicy metadata: name: pod-limited spec: allowPrivilegeEscalation: false # 不允许使用 Capabilities # allowedCapabilities: # - '*' fsGroup: rule: RunAsAny runAsUser: rule: RunAsAny supplementalGroups: rule: RunAsAny volumes: - configMap - downwardAPI - emptyDir - persistentVolumeClaim - secret - nfs
可想而知,在一个添加了上述限制的集群上,istio-init 容器由于无法获得相应特权,将无法完成预定工作:无法让 Envoy 代理软件捕获 Pod 中的流量。这样整个 Istio 软件的功能也就无从谈起了。此外,从 Kubernetes 1.21 版本开始, PSP 功能将逐步被弃用。新版集群上可能使用其他替代机制限制 Pod 权限。
上面谈到的安全风险来自于在所有需要注入 Sidecar 的业务 Pod 均需要同步注入高权限 istio-init 容器,而业务 Pod 可以由使用集群的任何人来创建和使用。这对于集群来说,就构成了攻击面的无限蔓延。这类问题的解决思路,通常是将攻击面集中化管理。也就是说,由少量可控的高权限 Pod 来侦听 Pod 创建的过程,在 Pod 启动前,为它们完成 IPTABLES 下发过程。
这正是 Istio CNI 所解决的问题。
通常,如果要侦听 Pod 创建、删除的事件,使用 Imformer 机制即可很轻松地获取到集群内各类资源的创建与回收事件。这也是在 Kubernetes 中开发各类 Controller 所常见的做法。但为 Pod 下发 IPTABLES 规则的任务与普通 Controller 有所不同:它需要在 Pod 事件创建或删除时,在 Pod 所在工作节点上,进入相应容器的 Linux Namespace,并下发 IPTABLES 规则。从执行位置来说,这更像是一种 Daemonset。另一方面,对 Pod 的创建和删除事件的处理恰好与 CNI 定义的一些命令吻合,CNI 是 Kubernetes 定义的用于为 Pod、Service 提供网络的机制。正好下发 IPTABLES 也是一种网络相关的操作,所以 Istio 团队也就索性直接以 CNI 插件的方式提供这一功能。
Istio CNI 的工作流程如下图:
Istio CNI DaemonSet 负责将 Istio CNI 插件的可执行程序安装到工作节点上。这些程序稍后在新的业务 Pod 创建或销毁时会收到来自 k8s 的调用,接着它们完成 IPTABLES 规则的配置。由于这些程序是运行在工作节点上,因此具有较高的权限:但它们可以被集中管理,因此权限是受控的,而业务 Pod 此时不再需要高权限来配置这些 IPTABLES,只需要运行一个简单的检查程序,确保业务容器运行之前,这些规则已就绪即可。
上图是 Istio 自注入模板的代码片断,从中可以看出,当启用 Istio CNI 时,如果启用了 Istio CNI 功能,Istio 向 Pod 注入的容器不再需要高权限。
与普通 CNI 插件不同,Istio CNI 并不提供常规的 IP 地址管理(IPAM)和联网(SDN)功能。它只在 Pod 的网络已建立之后,负责下发上述规则。因此,Istio CNI 并不会、也不能替换集群现有的 CNI 插件的功能。也就是说,在配置 Istio CNI 之外,k8s 集群还需要配置其他负责 IPAM 和 SDN 的软件,比如我们熟悉的 Flannel 或 Calico 等。
为了配合不同种类的现有 CNI 插件,Istio CNI 既能以“网卡插件”(Interface Plugins)的方式运行,也能以“插件链”(Chained Plugins)的方式附加到现有网卡插件运行。根据 CNI 标准的描述,网卡插件是用于创建虚拟网卡的 CNI 插件,而插件链则允许多个插件为已创建的网卡提供附加功能。插件链模式很符合 Istio CNI 的定位,也是 Istio CNI 的默认运行方式。在这种运行方式下,Istio CNI 先会检查集群当前 CNI 插件的配置:如果它已经是一个插件链,则将自身添加到它的尾部,成为新的功能;如果当前插件是一个“网卡插件”,则先将其转换为插件链,再将自身添加到链的尾部。
TKE Stack 是由腾讯主导的开源 k8s 发行版,与社区版 k8s 相比,TKE Stack 主要提供了更强的网络接入能力、多集群管理能力,以及将容器资源与业务和用户等因素集成管理等丰富的功能。TKE Stack 也是腾讯云提供的容器服务的开源版本,在腾讯内部部署了超过数十万核的超大规模集群,稳定运行了数年。
TKE Stack 的默认 CNI 插件是 Galaxy,它是一个能让集群接入各类网络插件的“元 CNI”框架:基于它,我们可以让集群中的 Pod 基于 Flannel 之类的插件获得普通 Overlay 网络的同时,还可以基于其他插件获得诸如 Underlay 网络等强大的能力。比如,典型的 Underlay 网络可以提供的能力有,可以让 Pod 获取到另一个子网(比如工作节点所在子网)的 IP 地址、获取固定 IP 地址等。
经过测试发现,在一些集群上,Istio CNI 插件默认的插件链运行模式与 Galaxy 不能兼容。原因是,Istio CNI 的配置转换处理过程存在瑕疵:这些集群上的原有 Galaxy CNI 的配置是网卡插件(即 00-galaxy.conf)模式, 经过 Istio CNI 的处理之后,相关配置无法被 Galaxy CNI 识别和处理。
具体原因是,Istio CNI 在将原有配置复制为插件链模式的过程中,会删除原配置中的 cniVersion 版本号(如果有),在新生成的插件链配置文件 00-galaxy.conflist 时,将此版本号强制改为 Galaxy CNI 尚未支持的 0.3.1。进入 Galaxy CNI 相关 daemonset 容器,并模拟执行 CNI 版本检查命令,可以发现此集群上 Galaxy CNI 支持的 cniVersion 最高为 0.2.0。相关源码可点击此处。
CNI_COMMAND=VERSION /opt/cni/bin/galaxy-sdn </etc/cni/net.d/00-galaxy.conf
在这样的 TKE Stack 集群中以插件链模式运行 Istio CNI 之后,将出现新 Pod 无法创建的问题。具体错误为:plugin galaxy-sdn does not support config version “0.3.1”。从 Pod 创建日志及 kubelet 上都可以找到这一错误信息。
更糟糕的是,即使此时卸载 Istio CNI,仍然不能恢复 Galaxy CNI 的功能。这是因为虽然 Istio CNI 卸载过程会尝试回退它做的修改,但是回退过程只是将 Istio CNI 相关内容从新创建的 conflist 格式配置中移除,而并未将 CNI 配置文件恢复为原始的 conf 格式,无法识别的版本号 0.3.1 被保留了下来。
此时,需要管理员登录每台集群工作节点,手工将 /etc/cni/net.d/00-galaxy.conflist 文件内的 plugins 数组字段的第一个元素提取出来,并保存为单独的 conf 文件: /etc/cni/net.d/00-galaxy.conf。删除正在创建中、但无法成功的 Pod,等待其重建,Pod 的创建功能应该能自动恢复。
明确了问题的缘由,要解决这些问题就很直接了。在 TKE Stack 集群中安装 Istio CNI 的两个思路是:
既然 Istio CNI 提供了网卡插件的运行方式,那启用它是一种比较轻松的处置方法。安装 Istio CNI 时,关闭 chained 参数即可以网卡插件的方式运行 Istio CNI。如果是为已有 Istio 集群补充安装 Istio CNI,则可能需要手工修改位于 istio-system 命名空间的 Istio 注入模板配置 configmap/istio-sidecar-injector 资源中的 values 数据。其中的 istio_cni 配置节:
"istio_cni": { "enabled": true, "chained": false }
需要注意的是,以网卡模式运行的 Istio CNI,会在 Pod 创建为其时添加 k8s.v1.cni.cncf.io/networks 注解(Annotation),以便通知集群上可以支持这个注解的 CNI 插件调用 Istio CNI 完成功能。Galaxy CNI 作为一个元 CNI 插件,是可以支持这个注解的。当 Galaxy 遇到这个注解时,将会跳过默认的 Galaxy 网络插件,而启用注解中配置的 CNI 插件。Istio CNI 并不实际提供联网功能,因此如果只运行 Istio CNI 会导致 Pod 无法获得正确的 IP,因此还是无法正确创建。以下代码片断来自 Istio 注入模板,从中可以看出其中的逻辑:
{{- if and (.Values.istio_cni.enabled) (not .Values.istio_cni.chained) }} {{ if isset .ObjectMeta.Annotations `k8s.v1.cni.cncf.io/networks` }} k8s.v1.cni.cncf.io/networks: "{{ index .ObjectMeta.Annotations `k8s.v1.cni.cncf.io/networks`}}, istio-cni" {{- else }} k8s.v1.cni.cncf.io/networks: "istio-cni" {{- end }} {{- end }}
从上面的代码中可以看出,Istio 模板尝试读取 Pod 上已有的注解值,并将 istio-cni 追加到末尾。这样,我们只需要在创建 Pod 时将 Galaxy 默认的网络配置名称以注解的方式提前列出,即可正确创建 Pod。从 kube-system 命名空间中的 configmap/galaxy-etc 配置的 DefaultNetworks 可以找到当前 Galaxy CNI 的默认网络名称。
kind: Pod metadata: annotations: k8s.v1.cni.cncf.io/networks: galaxy-flannel name: my-pod
实际测试结果表明,以网卡插件模式运行 Istio CNI,并在 Pod 上标记原有网络模式,即可在 TKE Stack 上成功运行 Pod 并正常使用 Istio 的各项功能。
虽然以独立网卡插件模式运行 Istio CNI 是可以解决 Pod 无法创建的问题的,但是由于需要向 Pod 上添加额外的注解,所以给应用开发者或者部署流水线增加了额外的复杂度,甚至有可能影响 Pod 使用 Galaxy CNI 提供的其他网络功能。比较理想的效果是,能像 Istio CNI 原生提供的那样,能透明地支持相关功能。
幸运的是,在最新的 1.0.8 版本 的 Galaxy CNI 的代码中,已经支持了 0.4.0 及之前版本的各个 cniVersion。因此将 Galaxy CNI 的版本升级到最新版,就能以默认插件链模式运行 Istio CNI 了。如果你的集群中的组件经过了自己团队的定制,则需要联系这些定制组件的开发团队核实他们所使用的上游版本,并提醒他们升级 Galaxy 组件的版本。
升级到最新版本的 Galaxy 组件之后,再运行相应的验证脚本,可以发现新版本的 Galaxy 已支持包括 0.3.1 在内的多个 cniVersion。
作为流行的服务网格软件,Istio 可以为微服务提供接近无侵入的强大流量治理能力和丰富的观测能力。而 Istio 这些能力都来源于它对来往业务容器的网络流量的完全捕获能力。
虽然 Istio 本身提供了多种在指定命名空间安装的特性,但将 Istio 作为一个集群级基础平台能力是众多团队的首选。而在一些公开的多租户集群、有特殊安全策略要求等复杂的集群环境,安装和运营 Istio 会面临一些独特的挑战。
本文简要介绍了在安全限制严格的集群中,要使用 Istio 流量治理功能所依赖的 IPTABLES 网络策略需要的 Istio CNI 插件的运行原理,以及要在 TKE Stack 集群中运行 Istio CNI 会遇到的问题和解决方法。运用这些方法,可以较好地使用较低的权限运行业务应用的同时,以兼容集群现有网络功能的方式,提供 Istio 的完整功能。