Kubernetes调度与安全设计:保护你的容器化环境

本文深入探讨了Kubernetes调度机制在安全设计中的关键作用,详细介绍了节点选择器、亲和性、污点与容忍度等核心调度功能,并提供了攻击者视角的威胁分析,旨在帮助构建更安全、隔离的容器工作负载环境。

Kubernetes调度与安全设计

2024年1月23日 - 作者:Francesco Lacerenza, Lorenzo Stella

在测试活动中,我们通常会分析设计选择和上下文需求,以便根据不同的Kubernetes部署模式提供适用的修复建议。调度在Kubernetes设计中经常被忽视。通常,各种机制会优先考虑,包括但不限于准入控制器、网络策略和RBAC配置。

然而,一个被攻陷的Pod可能允许攻击者横向移动到运行在同一Kubernetes节点上的其他租户。即使有其他安全措施,Pod逃逸技术或共享存储系统也可能被利用来实现跨租户访问。

拥有面向安全的调度策略有助于在全面的安全设计中降低工作负载被攻陷的整体风险。如果在调度决策中将关键工作负载分离开,被攻陷Pod的爆炸半径就会减小。这样做可以防止与共享节点相关的、从低风险任务到业务关键工作负载的横向移动。

攻击者在一个被攻陷且周围无物的Pod上

Kubernetes提供了多种机制来实现面向隔离的设计,例如节点污点或亲和性。下面,我们描述了Kubernetes提供的调度机制,并强调它们如何有助于实现可操作的风险降低。

将讨论以下应用调度策略的方法:

  • nodeSelector字段匹配节点标签;
  • nodeNamenamespace字段,基础且有效;
  • 亲和性和反亲和性,针对包含和排斥的约束类型扩展;
  • Pod间亲和性和反亲和性,在处理包含和排斥时,将标签匹配的重点放在Pod标签而非节点标签上;
  • 污点和容忍度,允许节点排斥或容忍Pod被调度;
  • Pod拓扑分布约束,基于区域、可用区、节点和其他用户定义的拓扑域;
  • 设计一个自定义调度器,以满足你的安全需求。

工作负载分离机制

如前所述,将租户工作负载彼此隔离有助于减少被攻陷邻居的影响。这是因为运行在特定节点上的所有Pod将属于单个租户。因此,能够从容器逃逸的攻击者只能访问该节点上的容器和挂载的卷。

此外,具有不同授权的多个应用程序可能导致特权Pod与挂载了PII数据或具有不同安全风险级别的Pod共享节点。

1. nodeSelector

在约束中,这是最简单的,只需在Pod规范中指定目标节点的标签即可。

示例Pod Spec

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: v1
kind: Pod
metadata:
  name: nodeSelector-pod
spec:
  containers:
  - name: nginx
    image: nginx:latest
  nodeSelector:
    myLabel: myvalue

如果指定了多个标签,它们将被视为必需条件(AND逻辑),因此调度只会发生在满足所有标签的Pod上。

虽然它在低复杂度环境中非常有用,但如果指定了许多选择器且节点无法满足,它很容易成为停止执行的瓶颈。因此,如果需要应用许多约束,就需要对分配给节点的标签进行良好的监控和动态管理。

2. nodeName

如果在Spec中设置了nodeName字段,kube调度器会直接将Pod传递给kubelet,然后kubelet尝试将Pod分配给指定的节点。

从这个意义上说,nodeName会覆盖其他调度规则(例如nodeSelectoraffinityanti-affinity等),因为调度决策是预定义的。

示例Pod spec

1
2
3
4
5
6
7
8
9
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
  - name: nginx
    image: nginx:latest
  nodeName: node-critical-workload

限制:

  • 如果spec中的节点没有运行或资源不足以托管它,Pod将不会运行。
  • 像AWS的EKS这样的云环境具有不可预测的节点名称。

因此,它需要对可用节点和分配给每个工作负载组的资源进行详细管理,因为调度是预定义的。

注意: 实际上,这种方法使调度器的所有计算效率优势无效,应仅应用于易于管理的小型关键工作负载组。

3. 亲和性和反亲和性

NodeAffinity功能使得能够根据节点的某些特征或标签为Pod调度指定规则。它们可用于确保Pod被调度到满足特定要求的节点上(亲和性规则),或避免将Pod调度到特定环境中(反亲和性规则)。

亲和性和反亲和性规则可以设置为“优先”(软)或“必需”(硬):

  • 如果设置为preferredDuringSchedulingIgnoredDuringExecution,则表示软规则。调度器将尝试遵守此规则,但可能并不总是如此,特别是如果遵守该规则会使调度变得不可能或具有挑战性。
  • 如果设置为requiredDuringSchedulingIgnoredDuringExecution,则为硬规则。除非满足条件,否则调度器不会调度Pod。如果条件不满足,这可能导致Pod保持未调度状态(Pending)。

特别是,可以利用反亲和性规则来保护关键工作负载不与非关键工作负载共享kubelet。通过这样做,计算优化的缺乏不会影响整个节点池,而只会影响包含业务关键单元的少数实例。

节点亲和性示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: v1
kind: Pod
metadata:
  name: node-affinity-example
spec:
  affinity:
   nodeAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
       - weight: 1
         preference:
          matchExpressions:
          - key: net-segment
            operator: In
            values:
            -  segment-x
      requiredDuringSchedulingIgnoredDuringExecution:
        nodeSelectorTerms:
        - matchExpressions:
          - key: workloadtype
            operator: In
            values:
            - p0wload
            - p1wload
  containers:
  - name: node-affinity-example
    image: registry.k8s.io/pause:2.0

该节点首选位于具有特定网络段标签的环境中,并且要求匹配p0p1workloadtype(自定义策略)。可以使用多种运算符,其中NotInDoesNotExist是可用于实现节点反亲和性的特定运算符。从安全角度来看,只有要求条件得到遵守的硬规则才有意义。preferredDuringSchedulingIgnoredDuringExecution配置应用于不会影响集群安全态势的计算配置。

4. Pod间亲和性与反亲和性

Pod间亲和性和反亲和性可以根据已经在节点上运行的Pod的标签来约束Pod可以被调度到哪些节点上。如Kubernetes文档中所述:

“Pod间亲和性和反亲和性规则的形式是’这个Pod应该(或者在反亲和性的情况下,不应该)在X中运行,如果X已经在运行一个或多个满足规则Y的Pod’,其中X是一个拓扑域,如节点、机架、云提供商可用区或区域,或类似的域,Y是Kubernetes试图满足的规则。”

反亲和性示例

1
2
3
4
5
6
7
8
9
affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchExpressions:
        - key: app
          operator: In
          values:
          - testdatabase

在上面的podAntiAffinity案例中,我们永远不会看到该Pod在运行testdatabase应用程序的节点上运行。

它适用于希望将某些Pod调度在一起或系统必须确保某些Pod永远不会被调度在一起的设计。特别是,Pod间规则允许工程师在同一执行上下文中定义额外的约束,而无需在节点组方面进一步创建分割。然而,复杂的亲和性规则可能会造成Pod处于Pending状态的情况。

5. 污点和容忍度

污点与节点亲和性属性相反,因为它们允许节点排斥一组不匹配某些容忍度的Pod。可以将污点应用于节点,使其排斥Pod,除非Pod明确容忍这些污点。

容忍度应用于Pod,它们允许调度器调度具有匹配污点的Pod。需要强调的是,虽然容忍度允许调度,但决策并不保证。

每个节点还为每个污点定义了一个关联的操作:NoExecute(影响正在运行的Pod)、NoSchedule(硬规则)、PreferNoSchedule(软规则)。

这种方法非常适合需要严格工作负载隔离的环境。此外,它允许创建不仅仅基于标签的自定义节点选择规则,并且不留下灵活性。

6. Pod拓扑分布约束

您可以使用拓扑分布约束来控制Pod如何跨集群分布在故障域中,例如区域、可用区、节点和其他用户定义的拓扑域。这有助于实现高可用性以及有效的资源利用率。

7. 还不满意?自定义调度器来救援

Kubernetes默认使用kube-scheduler,它遵循自己的一套标准来调度Pod。虽然默认调度器功能多样且提供了许多选项,但可能存在默认调度器不知道的特定安全要求。编写自定义调度器允许组织应用基于风险的调度,以避免将特权Pod与处理或访问敏感数据的Pod配对。

要创建自定义调度器,您通常需要编写一个程序来:

  1. 监视未调度的Pod
  2. 实现调度算法来决定Pod应该在哪个节点上运行
  3. 将决策传达给Kubernetes API服务器。

可以在以下GitHub仓库中找到可以为此进行适配的自定义调度器示例:kubernetes-sigs/scheduler-pluginsonuryilmaz/k8s-scheduler-example。此外,一个关于构建自定义调度器的精彩演讲是:Building a Kubernetes Scheduler using Custom Metrics - Mateo Burillo, Sysdig。正如演讲中提到的,由于其复杂性,这不适合胆小的人,如果你还没有计划构建一个,最好只坚持使用默认的。

攻击性提示:调度策略就像磁铁

如前所述,调度策略可用于将Pod吸引或排斥到特定的节点组。

虽然正确的策略可以减少被攻陷Pod的爆炸半径,但从攻击者的角度来看,仍然有一些方面需要注意。

在特定情况下,已实现的机制可以用于:

  • 吸引关键Pod - 一个被攻陷的节点或能够编辑元数据的角色可能被滥用来操纵受控节点的标签,从而吸引攻击者感兴趣的Pod。
    • 仔细审查可能被滥用来编辑元数据的角色和内部流程。验证内部威胁通过影响或更改标签和污点来利用这种吸引力的可能性。
  • 避免在关键节点上被排斥 - 如果用户应该提交Pod spec或对Pod的动态结构有间接控制,这可能被滥用于调度部分。能够提交Pod Spec的攻击者可以使用调度首选项跳转到关键节点。
    • 始终审查调度策略,以找出允许Pod降落在托管关键工作负载的节点上的选项。验证用户控制的流程是否允许添加它们,或者逻辑是否可能被某些内部流程滥用。
  • 阻止其他工作负载被调度 - 在某些情况下,了解或逆向应用策略可能允许特权攻击者制作Pod以在调度决策时阻止合法工作负载。
    • 寻找可能用于锁定节点上调度的标签组合。

附加部分:节点标签安全性

通常,kubelet仍然能够修改节点的标签,这可能允许被攻陷的节点篡改自己的标签以欺骗调度器,如上所述。

可以应用NodeRestriction准入插件来采取安全措施。如果标签中存在node-restriction.kubernetes.io/前缀,它基本上会拒绝来自kubelet的标签编辑。

总结:是时候做出调度决策了

从安全角度来看,为每个命名空间/服务设置专用节点将构成最佳设置。然而,这种设计不会利用Kubernetes优化计算的能力。

以下示例代表了一些权衡选择:

  • 在它们自己的节点组上隔离关键的命名空间/工作负载。
  • 为每个命名空间的关键Pod保留一个节点。
  • 为关键的命名空间部署一个完全独立的集群。

成功方法的核心概念是拥有一组为关键命名空间/工作负载保留的节点。现实世界的场景和复杂的设计要求工程师根据性能要求和风险承受能力来规划合适的机制组合。

这个决策始于定义工作负载的风险:

  • 不同的团队,不同的信任级别 大型组织中,多个团队部署到同一个集群的情况并不少见。不同的团队可能具有不同程度的可信度、培训或访问权限。这种多样性可能引入不同级别的风险。
  • 正在处理或存储的数据 一些Pod可能需要挂载客户数据或拥有持久密钥来执行任务。与任何安全加固程度较低的工作负载共享节点可能会使数据面临风险。
  • 同一节点上暴露的网络服务 任何暴露网络服务的Pod都会增加其攻击面。与外部请求交互的Pod可能会受到这种暴露的影响,并且更有可能被攻陷。
  • Pod权限和能力,或其分配的风险 某些工作负载可能需要一些权限才能工作,或者可能运行的代码本质上处理可能不安全的内容或第三方供应商代码。所有这些因素都可能导致工作负载的分配风险增加。

一旦找到环境中的风险集合,就决定团队/数据/网络流量/能力的隔离级别。如果它们是同一流程的一部分,对它们进行分组可能有效。

此时,每个隔离组中的工作负载量应该是可评估的,并准备好通过混合调度策略来解决,根据每个组的大小和复杂性。

注意: 简单的环境应使用简单的策略,如果存在很少的隔离组和约束,则应避免混合过多的机制。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计