TT Lab
开始
学习 学习路径 课程

KCSA — Kubernetes 安全助理

调度器、kube-proxy、容器运行时 — 安静组件的凭据

在 TT Lab 中继续学习

一句话总结

kube-scheduler 和 kube-proxy 各自握着对 API 服务器的客户端证书,并各自开着自己的 HTTPS 端口。容器运行时通过一个 Unix 套接字,创建和删除节点上所有的容器。这三个组件所处的位置,很难被审计日志捕捉到,所以必须 知道它们握着什么、开着哪里,并把那扇门收窄。

为什么需要它

KCSA 的集群组件安全领域,问的不只是 API 服务器、etcd 和 kubelet。攻击者不会去敲守得最严的那扇门。他们会找的是 管理员不怎么去看的凭据和端口,比如调度器的 kubeconfig、kube-proxy 的指标端口、运行时套接字。API 服务器绕过风险 这篇文档,就是从这个角度写的——不经过 API 服务器就能改变集群的路径,既不经过准入控制,也不经过审计日志。

工作原理

kube-scheduler——握有绑定权限的控制平面进程

根据 kube-scheduler 参考 文档,调度器是这样一个控制平面进程:针对调度队列中的每个 Pod,按约束条件和可用资源选出节点并排出名次,进行 绑定。这个绑定权限本身就是威胁模型。拿到了调度器的凭据(在 kubeadm 中是 /etc/kubernetes/scheduler.conf 里的客户端证书)的攻击者,可以把 Pod 放到想要的节点上,也可以把自己的 Pod 贴到与敏感工作负载相同的节点上。

调度器还会开自己的 HTTPS 端口。在端口与协议 表里是 10259。这个端口的认证和授权,由两个标志决定。--authentication-kubeconfig 指向能创建 TokenReview 的 kubeconfig,如果留空,所有令牌请求都会被当作匿名。--authorization-kubeconfig 指向能创建 SubjectAccessReview 的 kubeconfig,如果留空,凡不是跳过授权的请求都会被拒绝。也就是说,这两个标志要设置成委托给 API 服务器,指标和状态端点才不会变成谁都能读的门。--leader-elect 默认为 true,所以在多个副本的调度器中,只有一个在工作。

还要知道绕过调度器的路径。排空节点 文档写道,如果直接指定 Pod 的 nodeName,它不经过调度器就会被绑定到那个节点。有创建 Pod 权限的用户,可以无视调度器的策略自己选节点,所以对敏感节点的隔离,不能靠调度器配置,而要靠准入(PodNodeSelector 等)和污点来强制执行。

kube-proxy——每个节点上都在运行的网络代理

kube-proxy 参考 文档说明,kube-proxy 在每个节点上运行,把 API 中的 Service 定义反映到节点上,把 TCP、UDP、SCTP 转发给后端。Linux 的代理模式有 iptables(默认)、ipvs、nftables。kube-proxy 实现的是 Service,而不是执行 NetworkPolicy。策略的执行,是下一篇阅读要讲的 CNI 的事。

从安全角度要看两点。第一是凭据。PKI 证书与要求 文档写道,每个节点上都有 kube-proxy 用来向 API 服务器认证的客户端证书。能读取放在节点上的这个 kubeconfig 的进程,就能以 kube-proxy 的权限与 API 服务器对话。第二是开放的端口。--healthz-bind-address 的默认值是 0.0.0.0:10256,所以会在所有接口上接收状态请求,而 --metrics-bind-address 的默认值是 127.0.0.1:10249,只在本地 输出指标。如果为了监控把它放宽到 0.0.0.0:10249,节点网络里的任何人都能读到 kube-proxy 的内部状态。必须知道默认值为什么是本地,才可以放宽。

容器运行时——一个套接字就是整个节点

根据 CRI 文档,kubelet 以 gRPC 客户端连接运行时,端点是 Unix 套接字(containerd 是 /var/run/containerd/containerd.sock,CRI-O 是 /var/run/crio/crio.sock)。API 服务器绕过风险 文档里“容器运行时套接字”一节写下了威胁——能访问这个套接字的攻击者,可以启动新容器,或者与正在运行的容器交互,如果该节点上的容器里有 Secret,还可以把权限扩大到其他节点或控制平面。

文档提出的缓解措施在文件系统层面。把对套接字的文件访问限制为 root,用内核命名空间把 kubelet 与其他组件隔离,无论是直接还是通过上级目录,都禁止包含运行时套接字的 hostPath 挂载,把 hostPath 设为只读以免绕过目录限制,并限制用户对节点本身的访问。同一篇文档的“static Pod”一节也与运行时相连——由 kubelet 直接从清单目录启动的 static Pod 不受 API 服务器管理,所以能往那个目录里写东西的攻击者,可以不经过准入,就把使用 hostPath 的 Pod 放到节点上。如果 static Pod 在准入时失败,kubelet 不会把它注册到 API 服务器,但 Pod 仍然在节点上照常运行。

运行时隔离是相反方向的工具。RuntimeClass 文档说明,可以为每个 Pod 选择不同的运行时配置,在性能与安全之间取得平衡。需要高等级信息安全保证的工作负载,用硬件虚拟化的运行时来运行,获得额外的隔离,同时接受开销。在节点的 CRI 实现上配置处理程序并创建 RuntimeClass 对象之后,就用 Pod 规格的 runtimeClassName 来选择。

在现场相遇的样子

为了收集指标,把 kube-proxy 开放到了 0.0.0.0。Prometheus 说要从节点之外抓取 10249,于是放宽了 metricsBindAddress,结果节点网络里的任何 Pod,即使没有 hostNetwork,也能连上节点 IP 的 10249。如果要放宽,必须同时配上用 NetworkPolicy 或防火墙只允许采集器的规则。

CI runner 挂载了运行时套接字。为了构建镜像,而把 /var/run/containerd/containerd.sock 以 hostPath 放进去的 Pod,是一个能操纵节点上所有容器的 Pod。这正是文档要求禁止这种挂载的原因,也是 Pod Security Standards 的 baseline 禁止 hostPath 的原因。

接下来的理论要看什么

在下一篇阅读里,会看处在边界上的三个组件——真正执行 NetworkPolicy 的 CNI、hostPath 与 CSI 凭据交织在一起的存储,以及 kubeconfig 和 exec 插件暴露出来的客户端一侧的风险。