Kubernetes安全设计:调度策略与风险隔离实战指南

本文深入探讨Kubernetes调度机制在安全设计中的关键作用,涵盖节点选择器、亲和性、污点容忍、拓扑分布等多种策略,分析如何通过工作负载隔离减少攻击面,并提供攻击者视角下的安全加固建议。

Kubernetes调度与安全设计

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

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

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

拥有以安全为导向的调度策略有助于在全面的安全设计中降低工作负载被入侵的整体风险。如果在调度决策中将关键工作负载分离,被入侵Pod的影响范围就会减小。这样做可以防止从低风险任务到关键业务工作负载的横向移动,这些移动与共享节点相关。

攻击者在被入侵的Pod上,周围空无一物

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

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

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

工作负载分离机制

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

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

1. nodeSelector

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

示例Pod规范

 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会覆盖其他调度规则(例如nodeSelector、亲和性、反亲和性等),因为调度决策是预定义的。

示例Pod规范

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

限制:

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

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

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

3. 亲和性与反亲和性

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

亲和性与反亲和性规则可以设置为“preferred”(软规则)或“required”(硬规则):

  • 如果设置为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

该节点优先选择位于特定网络段(通过标签),并且要求匹配p0或p1 workloadtype(自定义策略)。

提供了多个运算符,其中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-plugins 或 onuryilmaz/k8s-scheduler-example。 此外,关于构建自己的调度器的一个很好的演示是《Building a Kubernetes Scheduler using Custom Metrics - Mateo Burillo, Sysdig》。正如演讲中提到的,由于其复杂性,这并不适合胆小的人,如果您不打算构建一个,最好只坚持使用默认的。

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

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

虽然正确的策略可以减少被入侵Pod的影响范围,但从攻击者的角度来看,仍有一些方面需要注意。

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

  • 吸引关键Pod - 能够编辑元数据的被入侵节点或角色可能被滥用以吸引攻击者感兴趣的Pod,方法是操纵受控节点的标签。
    • 仔细审查可能被滥用来编辑元数据的角色和内部流程。验证内部威胁是否可能通过影响或更改标签和污点来利用吸引力。
  • 避免在关键节点上被拒绝 - 如果用户应该提交Pod规范或间接控制其动态结构方式,这可能会被滥用于调度部分。能够提交Pod规范攻击者可能利用调度偏好跳到承载关键工作负载的节点。
    • 始终审查调度策略,以找出允许Pod部署到承载关键工作负载的节点的选项。验证用户控制的流程是否允许添加它们,或者逻辑是否可能被某些内部流程滥用。
  • 阻止其他工作负载被调度 - 在某些情况下,了解或反向应用策略可能允许特权攻击者制作Pod,以在调度决策时阻止合法工作负载。
    • 寻找可能用于锁定节点上调度的标签组合

附加部分:节点标签安全

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

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

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

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

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

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

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

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

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

一旦找到环境中的风险集,就要确定团队/数据/网络流量/能力的隔离级别。如果它们是同一流程的一部分,将它们分组可能会起到作用。

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

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

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