☰
从K8s到宿主机:容器隔离突破路径与安全加固实战
2026/10/14 4:56:18 网站建设 项目流程

节点失联、SSH 又被安全策略禁掉的那天下午,我盯着集群列表里那个NotReady的节点,第一次意识到一件事:Kubernetes 这套系统,不仅能管理容器,在某些配置下,它本身就是一把能打开宿主机的钥匙。当时 kubelet 和容器运行时双双异常,常规手段都进不去,最后是绕到 Kubernetes API 侧,创建一个临时特权 Pod,再用nsenter一头扎进宿主机 1 号进程所在的命名空间,才把问题定位清楚。

这个经历让我把几套"从 K8s 到宿主机"的方法反复整理了好多遍。这篇文章就把这些年在故障排查、安全评估中实际用过的方法做一个系统性梳理:既讲边界为什么能被打破,也讲防守方怎么把口子堵上。内容主要面向平台工程师、SRE 和做安全评估的同行。先说清楚:下面所有操作,默认都在你自有环境、且获得明确授权的范围内进行。越过了这条线,性质就完全变了。

1. 边界机制拆解:容器隔离为什么挡不住"宿主机视角"

1.1 隔离三件套,每一层都只是"规则"不是"墙"

想理解"边界为什么能突破",先得把容器的隔离机制看清楚。容器能跑在宿主机上,依赖内核三大件:Namespace 负责视图隔离,Cgroups 负责资源限额,Chroot 负责切换根目录。很多人默认这三层叠加起来就等同于一台独立虚拟机,这个误解恰好就是"边界被打破"的起点。

Namespace 本质上只是让进程"看不到"某些对象,不代表它"够不着"。它改的是进程视角里的进程列表、网络栈、挂载点、主机名这些东西。就像一个戴着特殊眼镜的人,眼镜会滤掉某些画面,但他手里的遥控器依然能控制整栋楼的设备。

Cgroups 管的是 CPU、内存、IO 这类资源配额,压根不涉及权限边界。它能限制你一天用多少度电,但管不了你能不能拆墙。Chroot 更接近一个"把房门钥匙留在房间里,嘴上说你不能出门"的规则。对普通进程来说,这个规则够用;但对 root 进程来说,只要内核没有额外拦截,它完全可以反手把门打开。

我常用一个生活类比:Namespace 是给你换了副只能看到部分房间的眼镜,Cgroups 是限定你一天只能用多少度电,Chroot 是把房门钥匙留在房里却告诉你"不许出门"。这三层全是"规则性限制"而不是"物理性隔离",真正的安全阀其实在第四层——Capabilities、Seccomp、AppArmor 这一堆内核安全机制。它们管的是"即使你是 root,能做的操作也要受清单限制"。

1.2 容器里的 root 和宿主机 root,在同一个 uid=0 上

这里有个绕不开的概念:容器里的 root 和宿主机上的 root,在内核眼中是同一个 UID 0。为什么容器里是 root,却不能轻易改动宿主机文件?正因为内核在第二层加了很多钩子,通过 Capabilities(把 root 的全量权限拆成几十个细粒度能力位)、Seccomp(限制系统调用)、AppArmor/SELinux(限制文件与进程访问范围)来做拦截。

一旦这些钩子被关闭或大幅削弱——最常见的就是privileged: true——容器里的 root 就几乎等价于宿主机 root。整个"突破边界"的技术逻辑因此变得非常简单:你不需要冲击内核漏洞,只需要把容器运行参数调到"主机的视域"里,原本的隔离层就形同虚设。

理解了这一点,后面每一条路径都变得很容易看懂。它们本质上是在回答一个问题:在 K8s 的配置平面上,有哪些开关被打开之后,容器和宿主机之间的"纸墙"会被直接捅穿。

2. 路径一:特权容器加 nsenter,直接插进宿主 PID 命名空间

2.1 privileged 背后到底打开了哪些开关

securityContext.privileged: true是 K8s 里分量最重的一个安全开关。一旦置为 true,容器创建时内核会放开绝大部分 capabilities,等于 root 该有的能力全部还给了容器内的 root;同时 seccomp 限制被移除,挂载文件系统、加载内核模块、操作设备节点这些高危动作全部放行。

一句话版本:privileged 容器不再是一个"受限容器",而是一个借容器外壳运行的宿主机进程,只是进程视角还被关在容器里。配合hostPID: true,它连最后那层视角限制也一并打开,容器内可以看到宿主机的完整进程表。

2.2 拉起一个调试用特权 Pod

这种形态在故障排查里其实很常见。节点 SSH 全面禁用、kubelet 挂了、容器运行时也起不来,但你还能从控制面拿到集群 API,就可以用下面这份 YAML 拉一个调试容器到目标节点上。

apiVersion: v1 kind: Pod metadata: name: node-debugger namespace: kube-system spec: hostPID: true hostNetwork: true hostIPC: true containers: - name: debugger image: alpine:3.19 command: ["sleep", "infinity"] securityContext: privileged: true volumeMounts: - name: host-root mountPath: /host volumes: - name: host-root hostPath: path: /

这份清单里三个host*字段分别把进程、网络、IPC 的命名空间都换成了宿主视角;privileged 打开权限;挂在/host的 hostPath 则把整个宿主根目录拖进容器。生产环境查询节点状态、排查网络问题、捞日志文件,这个姿势几乎能覆盖所有场景。

command: ["sleep", "infinity"]这个细节别省。容器如果没有常驻进程,启动即退出,exec 都来不及执行,这个命令就是给 debug Pod 续命用的。

2.3 拿到宿主机 Shell 的关键两条命令

Pod 进入 Running 状态后,先 exec 进容器:

kubectl exec -it -n kube-system node-debugger -- sh

此时因为开了hostPID: true,容器里能看到宿主的 1 号进程:

ps -ef | head -5

正常情况下你会看到 systemd 或你所用发行版的 init 进程。接着就是核心一步:

nsenter -t 1 -m -u -i -n -- sh

这里依次解释一下参数:-t 1指定目标进程为 PID 1;-m进入它的挂载命名空间;-u进入 UTS 命名空间(主机名视角);-i进入 IPC 命名空间;-n进入网络命名空间。执行完这条命令,Shell 提示符可能还是容器里的,但你的视角已经在宿主机这边了——看到的进程、网络、挂载点全是宿主机视角,能操作的范围也跟随进程权限走。

这段操作的本质是:用 K8s 的调度能力把特权容器送到目标节点,再用nsenter完成从容器命名空间到宿主命名空间的跳转。整个过程完全基于内核原生机制,不存在任何"逃逸漏洞",纯粹是配置层面把门打开。

2.4 几个值得警惕的实操细节

第一,没有hostPID: true的话,容器里根本看不到宿主 1 号进程,nsenter -t 1会失败。有些调试镜像会改用-t 1加上对/proc/1/root的引用,但最稳的方案还是声明hostPID。

第二,特权容器不是必须挂载/host,但挂载后排查起来方便不少。宿主的 /var/log 下那一堆 kubelet、容器运行时日志,翻起来效率极高。

第三也是最重要的一条:用完立刻删。kubectl delete pod node-debugger -n kube-system。特权容器在集群里多留一秒,都是给攻击者递刀。实际运维中我见过有人把这种调试 Pod 留在集群里当"后门"用,这是非常危险的习惯。

3. 路径二:kubelet API 的匿名访问与静态 Pod 投递

3.1 10250 端口上的"侧门"是怎么开的

kubelet 是每个节点上的核心代理,默认监听 10250 端口,对外提供 /pods、/exec、/run 等一堆 API。这套 API 本来是给 kube-apiserver 调用的,但很多人忽略了它直接暴露在节点网络上的事实。

如果 kubelet 的认证配置不当——authentication: anonymous: enabled被打开,且 webhook 认证未启用——那么任何能触达节点 10250 端口的人,都可以直接调用 kubelet API。被访问控制策略隔离的容器网络环境里,这个风险相对可控;但一旦某个 Pod 能做到主机网络(hostNetwork),或者集群网络本身没有 ACL 隔离,问题就大了。

验证方法很简单:

curl -k https://<node-ip>:10250/pods

如果返回一长串 Pod JSON,而不是 401/403,说明匿名访问是通的。这也是安全评估时最先测试的点位。

3.2 在节点上跑命令的两种姿势

旧版 kubelet 提供过/run/{namespace}/{pod}/{container}?cmd=id这样的端点,直接 POST 就能在目标容器里执行命令。新版本改走/exec配合 WebSocket streaming,纯 curl 不太好使,要自己写 WS 客户端封装,复杂度高不少。

更稳定的是走"静态 Pod 投递"链路。如果能拿到节点文件系统的写权限,直接往/etc/kubernetes/manifests/目录下丢一个 YAML 文件,kubelet 会自动检测并拉起这个静态 Pod。这是 K8s 自身组件(etcd、kube-apiserver)的部署方式,所以 kubelet 对静态 Pod 有天然信任。

下面这个 manifest 一旦出现在节点 manifests 目录,kubelet 会在几十秒内自动调度:

apiVersion: v1 kind: Pod metadata: name: node-agent namespace: kube-system spec: hostPID: true hostNetwork: true containers: - name: agent image: alpine:3.19 command: ["sh", "-c", "sleep infinity"] securityContext: privileged: true

要形成完整攻击链,还需要先拿到写 manifests 目录的权限。这个权限从哪来?hostPath 挂载、某个节点上的文件写漏洞、或者已经拿到宿主机低权限用户,都是常见前置条件。这条链路的重点是:kubelet 对静态 Pod 的信任度极高,你不需要经过控制面 API,节点自身的 kubelet 就会帮你完成任务。

3.3 自查清单:你的 kubelet 有暴露风险吗

检查命令没有想象中复杂。先看端口验证:

curl -k https://127.0.0.1:10250/pods -o /dev/null -w "%{http_code}\n"

返回 401/403 说明认证有效,返回 200 就要立即整改。再看 kubelet 配置:

cat /var/lib/kubelet/config.yaml

重点看authentication段:anonymous必须是false,webhook应保持开启。很多发行版的 kubeadm 默认配置是安全的,但手动部署的集群经常出现漏配。

4. 路径三:hostPath 挂载根目录后的文件级越权与 chroot

4.1 hostPath 的信任边界:一瓶水装进了保险柜

hostPath 卷的初衷是让容器读取宿主机特定目录,最常见的是挂载 /var/log 给日志采集器、挂载容器运行时 socket 给管理工具。这些场景是"定向开窗"——窗口只开一条缝,够用就行。

但实际操作里,我见过太多直接hostPath: path: /的配置,等于把整个宿主机文件系统倒进了容器,却只给容器套了一层薄薄的命名空间。这种配置下,即使容器没开 privileged,普通 root 用户已经能对宿主机文件"读所欲读、写所欲写"。

危害等级可以直接拉满。秘密文件、SSH 私钥、数据库备份、其他 Pod 挂在宿主机上的业务数据,全都会暴露在容器的可读范围内。

4.2 chroot 一下,进入宿主文件系统视角

假设你拿到一个挂载了根目录的容器的执行权限:

kubectl exec -it hostpath-pod -- sh

容器内能看到/host下就是宿主机根目录。接着:

chroot /host bash

执行完chroot,当前 shell 的/不再是容器镜像里的根,而是宿主的真实根。由于镜像里有 bash 的话,chroot 后直接可用;没有 bash 就用chroot /host sh。这一步之后,读取宿主机的 /etc/shadow、/root/.ssh/、/etc/kubernetes/pki 等目录都会变得畅通无阻。

值得强调的是,这条路径不依赖 privileged,也不依赖任何内核漏洞,纯粹是配置层面的"逻辑漏洞"。而且它足够隐蔽——很多审计系统只盯着 privileged 容器,却完全忽略 hostPath 根目录挂载。

4.3 一个典型场景:写 SSH 公钥

挂载根目录带来的写权限,真正严重的地方在于"持久化"。比如给宿主的 root 用户写一个公钥:

mkdir -p /host/root/.ssh echo 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQ...' >> /host/root/.ssh/authorized_keys

如果目标节点对外开放了 SSH 22 端口,这把公钥就是一道常开的门。对比 transient 的/run端点,这种文件级操作不需要连通 10250,也不需要特权标记,防御上更难被实时捕获。点到为止,不再往下接恶意利用细节。重点是要明确一个结论:hostPath 根目录挂载本身就是高危配置,和有没有 privileged 没有关系。

4.4 防守姿势:用 Pod Security Admission 卡住高危挂载

K8s 从 1.23 起用 Pod Security Admission(PSA)取代了旧版 PSP。给命名空间打上restricted标签,能挡住 privileged 容器;但 PSA 对 hostPath 的限制没有到"禁止根目录"的粒度,它只区分 hostPath 是否存在。所以真正的防线还是在准入控制层或者云平台自身的策略引擎里做白名单。

推荐的管理方式:所有使用 hostPath 的 Pod 都必须声明路径前缀白名单,例如只允许/var/log、/var/lib/docker/containers这类明确目录,禁止/、/etc、/root、/var/lib/kubelet等敏感根区域。这个白名单建议下沉到变更流程里,靠人工 review YAML 的方式迟早会有漏网。

5. 突破之后:宿主机上的信息落点与巡检痕迹

5.1 拿到宿主机视角后,通常能捞到什么

从安全评估视角看,拿到宿主机命令执行权限后,红队会重点检查哪些位置,蓝队就应该反过来盯这些位置。下面是几个高频落点:

落点价值加固方向
/root/.ssh/ 私钥与 known_hosts横向移动到其他主机SSH 密钥统一托管,禁止私钥长期落在宿主机
/etc/kubernetes/ 下证书伪造节点身份或 K8s 组件身份证书轮换,DBaAS 管理密钥
ServiceAccount Token继承 Pod 的 K8s API 权限最小 RBAC,Token 绑定时效
其他容器的 volume 数据隔壁业务的数据泄露数据卷用加密文件系统,权限隔离
镜像仓库凭据改镜像供应链仓库私有化,短期 token 取代长密钥

拿hostPID之后,/proc里还能看到同节点所有进程的 cmdline、环境变量,有些应用在环境变量里塞数据库密码和 API Key,这就等于把密钥集中存放了。所以蓝队巡检时,第一步就应该看 node 上是否存在异常的特权容器,以及是否有 hostPath 卷指向根目录。

5.2 蓝队巡检时盯紧的四个信号

具体的巡检思路可以落到这几点:第一,集群里有没有来历不明的privileged: truePod,哪怕它伪装成监控组件;第二,节点 manifests 目录有没有新增的静态 Pod 文件,这个目录正常情况下一手可数,新增文件要立刻告警;第三,kubelet API 的匿名访问是否被打开,这是最容易被忽视的侧门;第四,审计日志里有没有非常规时间点的exec操作,K8s 审计策略要能记录到 Pod 内执行命令这件事。

配合命令可以这样查:

kubectl get pods -A -o json | jq '.items[] | select(.spec.containers[].securityContext.privileged == true)'

这个查询会把所有特权容器列出来,对照命名空间和 Owner,能快速发现异常。再查一下 hostPath 根挂载:

kubectl get pods -A -o json | jq '.items[] | .spec.volumes[]? | select(.hostPath.path == "/")'

结果里凡是把根目录挂进容器的 Pod,要么是明确的诊断工具,要么就是大问题。

6. 防守侧:三条最有效的加固红线与运行时检测

6.1 红线一:给每个命名空间贴上安全基线标签

PSA 的三个级别里,restricted级别能挡住 privileged 容器、hostPID、hostNetwork 等大部分关键高危项。直接把标签压到命名空间上:

kubectl label ns default pod-security.kubernetes.io/enforce=restricted kubectl label ns kube-system pod-security.kubernetes.io/enforce=baseline

对 kube-system 这类系统命名空间用baseline,对业务命名空间直接上restricted,是防守性价比最高的起步配置。这个操作五分钟就能完成,却会把大量低质量的"裸奔配置"挡在门外。

6.2 红线二:kubelet 必须关匿名、开 webhook

kubelet 的安全配置应该作为节点基线检查项,写进准入脚本。检查点固定为:authentication.anonymous为 false、authorization.mode为 Webhook、readOnlyPort为 0(禁用只读端口)。这几项配置由 kubeadm 默认配置保证,但手动部署节点时经常漏。

顺带说一句:有些部署方案在云厂商托管环境里已经默认改好,但自建集群仍然是最容易踩坑的地方。

6.3 红线三:运行时检测覆盖"容器行为异常"

配置层面的防线始终有疏漏,运行时检测能兜底。Falco 这类工具最擅长捕捉"容器内出现特权操作"和"进入宿主命名空间"这类行为。

建议重点盯两类事件:一类是 容器内执行nsenter、chroot、mount这类高危系统调用,另一类是 节点 manifests 目录有新增文件。这两个事件只要出现,基本可以认定有异常行为在发生,无论它打着运维排查的旗号还是安全评估的旗号,都应该至少弹出一个高优先级告警让值班人员确认。

6.4 加固之后不要忘了看一眼审计日志

K8s 审计日志默认可能没开全,需要确认 apiserver 审计策略里是否记录了exec和attach操作。如果审计里压根看不到 exec 记录,那么蓝队就只能靠运行时工具补救。配置一个精简的审计策略,把pods/exec和pods/attach的级别设为RequestResponse,就能完整记录谁、在什么时间、对哪个容器执行了命令。

这些加固手段本身都不复杂,难的是坚持。实际生产环境里,特权容器往往会因为"图省事"从一个变成一百个,等到出事故时再回头排查,成本翻倍。所以我给自己团队定的规矩是:任何特权容器都必须附带工单号和过期时间,到期自动回收。这个习惯看起来笨,但真到紧急排查的那一天,你会庆幸自己能一眼看清集群里的"钥匙"都在谁手里。

最后再说一个个人体会:这个主题之所以值得反复拿出来讨论,是因为它在运维与安全之间划出了一条很细的线——同一套命令,在授权范围内是排障利器,在授权范围外就是严重事故。能力本身没有对错,边界感才是真正的分水岭。写完这份整理,我自己又把集群里的特权容器和 hostPath 根挂载查了一遍,事实证明,查出来的比预想的多。希望你也抽空查一遍。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询