Kubernetes 准入控制器安全加固:NodeRestriction 与 DenyServiceExternalIPs 落地验证

枫少@KillBoy
枫少@KillBoy
枫少@KillBoy
管理员
344
文章
0
粉丝
云原生安全1 21字数 2809阅读9分21秒阅读模式
AI智能摘要
当攻击者突破应用层权限后,常会瞄准 kubelet 身份滥用或 Service 流量劫持这类控制面弱点。本文聚焦 NodeRestriction 与 DenyServiceExternalIPs 两个准入插件,前者封堵 kubelet 越权修改 Node 与 Pod 的通道,后者掐断 CVE-2020-8554 描述的 externalIPs 流量劫持路径。从风险模型到参数配置、攻击模拟与回滚预案,这套落地流程能否真正堵住控制面漏洞?
— AI 生成的文章内容摘要

Kubernetes 控制平面的安全防线中,准入控制器(Admission Controller)扮演着请求“守门员”的角色:它在请求通过认证和鉴权之后、对象被持久化之前拦截 API 请求,既可以修改请求内容,也可以直接拒绝。当攻击者突破应用层权限后,往往会把目光投向 kubelet 身份滥用或 Service 流量劫持这类控制面弱点。本文聚焦两个关键准入插件——NodeRestriction 与 DenyServiceExternalIPs,前者封堵 kubelet 越权修改 Node 与 Pod 对象的通道,后者直接掐断 CVE-2020-8554 所描述的 Service externalIPs 流量劫持路径。文章将从风险模型讲起,给出从参数配置、攻击模拟到回滚预案的完整落地流程。

Kubernetes 准入控制器在 API 服务器请求链路中的位置示意图

风险模型:两个插件分别解决什么问题

在动手配置之前,先厘清两个插件所对应的威胁场景。NodeRestriction 解决的是 kubelet 身份的权限边界问题。默认情况下,kubelet 使用 system:nodes 组的凭证访问 API 服务器,在没有额外约束时,一个被攻陷的 kubelet 理论上可以修改任意 Node 对象、删除任意节点,甚至篡改节点上的污点(Taint)来干扰调度。NodeRestriction 准入插件会将 kubelet 的操作范围严格限制为:只能修改自己的 Node 对象、只能修改绑定到自身节点的 Pod 对象,并且禁止 kubelet 更新或删除 Node 对象的污点,对 kubernetes.io/ 或 k8s.io/ 前缀标签的修改也施加了严格限制。

DenyServiceExternalIPs 则针对 Service 的 .spec.externalIPs 字段。这个字段最初是为了给非云集群提供类似云负载均衡器的功能,但它的设计假设是“集群中的每个用户都完全可信”。在共享集群或多租户场景下,任何有权限创建或修改 Service 的用户都可以将 externalIP 设置为任意 IP 地址,从而劫持原本流向该 IP 的流量。这正是 CVE-2020-8554 描述的核心问题。由于该字段“非常强大且无法很好地受策略控制”,Kubernetes 官方自 1.21 起建议所有用户禁用此特性,并在 1.36 版本中正式弃用了 externalIPs 字段。DenyServiceExternalIPs 准入控制器的作用,就是拒绝所有对 externalIPs 的新增使用。

实施准备:确认版本与现状基线

开始配置前,需要确认三件事:集群版本、当前准入控制器列表、以及集群中是否有存量 Service 正在使用 externalIPs。

首先确认 Kubernetes 版本。NodeRestriction 自 1.7 起进入 Beta,1.8 起 GA;DenyServiceExternalIPs 在 1.21 引入,目前是默认禁用状态。如果你的集群版本低于 1.21,需要先升级控制平面。其次,检查当前 API 服务器启用的准入控制器列表:

kubectl -n kube-system get pod -l component=kube-apiserver -o jsonpath='{.items[0].spec.containers[0].command}' | tr ' ' 'n' | grep enable-admission-plugins

如果输出为空,说明使用的是默认准入控制器集合。需要特别注意的是,NodeRestriction 在 Kubernetes 1.10 之后已包含在默认启用列表中,但很多发行版或托管平台出于兼容性考虑会显式覆盖该列表。务必确认 NodeRestriction 是否出现在最终生效的列表中。

然后扫描存量 Service,评估启用 DenyServiceExternalIPs 的潜在影响面:

kubectl get svc --all-namespaces -o json | jq '.items[] | select(.spec.externalIPs != null) | {namespace: .metadata.namespace, name: .metadata.name, externalIPs: .spec.externalIPs}'

如果没有任何输出,说明集群中不存在使用 externalIPs 的 Service,可以安全启用 DenyServiceExternalIPs。如果有存量使用,则需要逐项评估这些 Service 的业务必要性,并规划替代方案(如 LoadBalancer、NodePort 或自定义网络策略)。

实施步骤:启用两个准入插件

修改 API 服务器参数

对于自建集群(kubeadm 部署),编辑 /etc/kubernetes/manifests/kube-apiserver.yaml,在 --enable-admission-plugins 参数中追加两个插件名。如果该参数不存在,则新增一行:

spec:
  containers:
  - command:
    - kube-apiserver
    - --enable-admission-plugins=NodeRestriction,DenyServiceExternalIPs

注意:如果原本已配置了其他准入插件(例如 PodSecurity,NodeRestriction),需要将新插件追加到同一列表中,以逗号分隔,切勿覆盖原有配置。修改后 kubelet 会自动检测清单文件变化并重启 API 服务器静态 Pod。

对于托管集群(如 EKS、ACK、GKE),通常在创建集群时通过控制台或 CLI 指定准入插件。以 EKS 为例,在创建或更新集群时指定:

eksctl create cluster --name my-cluster --enable-admission-plugins NodeRestriction,DenyServiceExternalIPs

对于 K3s 等轻量发行版,可通过 kube-apiserver-arg 配置项传入:

k3s server --kube-apiserver-arg="enable-admission-plugins=NodeRestriction,DenyServiceExternalIPs"

验证插件生效

API 服务器重启后,确认插件已加载:

kubectl -n kube-system get pod -l component=kube-apiserver -o jsonpath='{.items[0].spec.containers[0].command}' | tr ' ' 'n' | grep enable-admission-plugins

输出应包含 NodeRestriction 和 DenyServiceExternalIPs。更可靠的验证方式是直接构造被拒绝的请求,进入下一步攻击模拟。

攻击模拟与效果验证

验证 DenyServiceExternalIPs:构造 externalIPs 请求

创建一个带 externalIPs 的 Service,预期被准入控制器拒绝:

kubectl create service clusterip test-external --tcp=80:80 --dry-run=client -o yaml | sed '/^metadata:/a  externalIPs:n  - 203.0.113.10' | kubectl apply -f -

预期输出:

Error from server (Forbidden): error when creating "STDIN": admission webhook "deny-service-external-ips" denied the request: Service "test-external" is forbidden: use of externalIPs is forbidden

再验证对现有 Service 的修改同样被拦截。先创建一个正常 Service,然后尝试为其添加 externalIPs:

kubectl create service clusterip test-svc --tcp=80:80
kubectl patch svc test-svc --type=merge -p '{"spec":{"externalIPs":["203.0.113.11"]}}'

预期同样返回 Forbidden 错误。同时验证删除 externalIPs 的操作不受影响(如果存量 Service 存在该字段)。

验证 NodeRestriction:模拟 kubelet 越权

NodeRestriction 的验证需要模拟 kubelet 身份发起请求。这里使用 kubelet 的证书或创建一个具有 system:nodes 组权限的 ServiceAccount 来模拟。更简洁的方式是直接使用节点上的 kubelet 客户端证书发起 API 请求。

首先获取节点名称和 kubelet 证书路径(以 kubeadm 集群为例):

NODE_NAME=$(kubectl get nodes -o jsonpath='{.items[0].metadata.name}')
sudo cat /var/lib/kubelet/pki/kubelet-client-current.pem > /tmp/kubelet-client.pem
sudo cat /var/lib/kubelet/pki/kubelet-client-current-key.pem > /tmp/kubelet-client-key.pem

使用 curl 以 kubelet 身份尝试修改其他节点的标签:

curl -k --cert /tmp/kubelet-client.pem --key /tmp/kubelet-client-key.pem 
  -X PATCH 
  -H "Content-Type: application/merge-patch+json" 
  -d '{"metadata":{"labels":{"kubernetes.io/hostname":"hacked"}}}' 
  "https://<API_SERVER_ADDR>/api/v1/nodes/<OTHER_NODE_NAME>"

预期返回 403 Forbidden。NodeRestriction 会拒绝 kubelet 修改其他节点的标签,尤其是 kubernetes.io/ 和 k8s.io/ 前缀的标签。

再验证 kubelet 修改自身节点污点的行为同样被拒绝:

curl -k --cert /tmp/kubelet-client.pem --key /tmp/kubelet-client-key.pem 
  -X PATCH 
  -H "Content-Type: application/merge-patch+json" 
  -d '{"spec":{"taints":[{"key":"dedicated","value":"test","effect":"NoSchedule"}]}}' 
  "https://<API_SERVER_ADDR>/api/v1/nodes/$NODE_NAME"

预期同样返回 403。这两个测试验证了 NodeRestriction 对 kubelet 越权操作的核心拦截能力。

验证准入控制器拦截效果的终端操作场景

业务兼容性评估与风险处置

两个插件的业务影响面差异较大。DenyServiceExternalIPs 需要重点评估。在启用前,务必完成以下排查:

  • 存量 externalIPs 使用清单:逐项确认每个使用 externalIPs 的 Service 是否仍然必要。对于确需保留的场景,官方建议使用自定义策略(如 ValidatingAdmissionPolicy 或 OPA/Gatekeeper)来管理 externalIPs 的使用,而不是完全放开。
  • 替代方案评估:对于非云集群中需要类似负载均衡器功能的场景,可考虑 MetalLB 等裸机负载均衡器方案,或使用 NodePort 配合外部负载均衡设备。
  • 兼容性窗口:由于 Kubernetes 1.36 已正式弃用 externalIPs,且未来版本将从 kube-proxy 中移除该行为的实现,建议制定迁移时间表,逐步替换所有存量使用。

NodeRestriction 由于默认已启用且限制的是 kubelet 的越权能力,对正常业务几乎没有影响。唯一的注意点是,如果集群中有自定义控制器或运维脚本以 kubelet 身份执行跨节点操作,需要改为使用独立的 ServiceAccount 和 RBAC 权限。

监控观察点与回滚预案

监控观察点

启用插件后,建议关注以下指标和日志:

  • API 服务器拒绝请求计数:通过 apiserver_admission_controller_admission_duration_seconds 和 apiserver_admission_webhook_rejection_count 等 Prometheus 指标,观察新增拒绝请求的趋势。
  • 审计日志:在 API 服务器审计策略中,为 create 和 update 操作添加对 services 资源的审计记录,重点关注 decision=forbid 的事件。
  • 节点状态异常:NodeRestriction 启用后,如果出现 kubelet 无法更新自身节点状态的情况(通常表现为 Node 对象长时间未更新),需要检查 kubelet 证书是否仍然有效、是否使用了正确的 system:node:<nodeName> 用户名格式。

回滚策略

如果启用后发现严重的业务兼容性问题,回滚操作相对简单。将 --enable-admission-plugins 参数中的对应插件名移除,保存配置文件并等待 API 服务器重启即可。对于 kubeadm 集群:

  1. 编辑 /etc/kubernetes/manifests/kube-apiserver.yaml,从 --enable-admission-plugins 列表中移除 DenyServiceExternalIPs(或同时移除 NodeRestriction)。
  2. 保存文件后,kubelet 会自动检测变化并重启 API 服务器静态 Pod。
  3. 重启完成后,验证插件是否已从列表中移除。

回滚的影响范围:移除 DenyServiceExternalIPs 后,集群恢复允许创建带 externalIPs 的 Service,但存量被拒绝的请求不会自动重放,需要业务方重新提交。移除 NodeRestriction 后,kubelet 的越权限制解除,但节点鉴权(Node Authorization)仍然会提供基础的权限控制,只是不再有对象级别的细粒度约束。

复盘:加固的边界与后续动作

两个插件的落地验证完成后,需要明确它们的防护边界。NodeRestriction 只约束 kubelet 对 Node 和 Pod 对象的修改,它不限制 kubelet 读取 Secret、ConfigMap 等资源(这些由节点鉴权器的读取规则控制),也不替代 RBAC 对用户权限的管理。DenyServiceExternalIPs 只拦截 externalIPs 的新增使用,不拦截存量字段的读取和删除,也不阻止用户通过其他方式(如 LoadBalancer 类型 Service 的 loadBalancerIP 字段)实现类似效果。

基于此,建议在启用这两个插件后,继续推进以下加固措施:

  • 为确需使用 externalIPs 的场景制定 ValidatingAdmissionPolicy,基于命名空间或标签进行白名单管控。
  • 在安全检查清单中加入这两项配置的定期核查,CIS Kubernetes Benchmark 的 1.2.3 项明确要求确保 --enable-admission-plugins 中包含 DenyServiceExternalIPs。
  • 关注 Kubernetes 版本升级节奏,在升级到 1.36+ 后,逐步清理存量 externalIPs 使用,为最终移除做准备。

准入控制器的价值在于把安全边界前移到 API 请求的入口,而不是依赖运行时的被动检测。NodeRestriction 与 DenyServiceExternalIPs 的组合,分别封堵了 kubelet 身份滥用和 Service 流量劫持两条关键攻击路径。通过本文的配置、模拟验证和回滚预案,你可以在可控风险下完成这两项控制面的安全加固,并为后续更细粒度的准入策略(如 ValidatingAdmissionPolicy)打下基础。

 
枫少@KillBoy
    • 甜蜜的回忆
      甜蜜的回忆 2

      这个攻击模拟写得挺细的,正好照着验证一遍

    匿名

    发表评论

    匿名网友

    拖动滑块以完成验证