K8s nodes/proxy权限滥用:从过度授权到集群RCE的攻防拆解
发布时间:2026/10/14 7:20:10 锦皓数字建站

上个月帮一家客户做集群安全巡检时我在某个业务命名空间里发现了一个粒度极不合理的 ClusterRoleBinding一个仅用于读取 Pod 日志的服务账号居然被绑定了system:node这个高危角色而绑定它的运维同事只是为了让监控面板能拉到节点指标。这种“图省事”的授权方式在真实环境里远比想象中常见而nodes/proxy权限的滥用正是这类过度授权链条里最容易滑向 RCE 的突破口。nodes/proxy是 Kubernetes 内置 RBAC 体系中一个极其特殊的子资源权限。拿到了它就等于拿到了一把绕过 API Server 鉴权语义直通 kubelet 的钥匙可以通过kubectl proxy或直接请求 API Server 的特定路径访问节点上 kubelet 的 HTTPS 端口进而调用 exec、attach、logs 这类高危接口。换句话说只要攻击者有机会执行kubectl命令或者拿到一个有该权限的 ServiceAccount Token就能在集群内的任意节点容器里执行命令后果直接对标远程代码执行。这篇文章我会从权限本质、攻击链路、落地防护、复现过程到踩坑记录把nodes/proxy滥用这件事彻底讲透。适合正在做集群安全加固的运维同学、平台团队的安全负责人也适合对 K8s 攻击面感兴趣的研究者按文章里的步骤在自己的测试环境里完整验证一遍比听我说一百遍都管用。1. 先说结论nodes/proxy 权限为什么值得重新审视很多团队的 K8s 权限治理核心目标就是把cluster-admin这类角色锁起来认为只要没人拿到集群管理员权限RBAC 就算安全了。但nodes/proxy的危险之处在于它不属于集群管理员角色的范畴却拥有接近集群管理员的上限破坏力。从权限模型的视角来看Kubernetes 的 RBAC 针对的是 API Server 暴露的各种资源和动词而nodes/proxy这个权限的逻辑是允许调用者对节点资源下的proxy子资源发起代理请求。API Server 收到这类请求后会把 HTTP 请求转发到目标节点的 kubelet 端口上相当于在 API 网关上开了一条去往节点管理端口的代理通道。这条通道的可怕之处在于它让“管控节点”这件事脱离了节点级别的网络访问控制。正常情况下如果某个角色需要管理节点上的 kubelet网络层面应该允许它直接访问节点的 10250 端口这种暴露很容易被防火墙或者集群网络策略拦截。但通过nodes/proxy攻击者只需要跟 API Server 通信不需要跟节点通信。API Server 是每个走集群服务的人必然要访问的组件这就让“网络可达性”这个天然屏障彻底失效了。另外值得注意的一点是nodes/proxy不是普通的 RBAC 权限它出现在几个系统内置的 ClusterRole 里。system:node角色的规则里就包含nodes/proxy的 get 权限而system:nodes这个角色则授权了所有节点相关操作。如果运维人员为了图省事把某个服务账号绑定了这几个角色那权限的风控基本就形同虚设了。我在巡检中发现凡是绑定了system:node或包含nodes/proxy的自定义 ClusterRole 的服务账号几乎都是因为某个监控组件、某个排障流程或者某次临时操作而创建的。这类账号长期存留在集群里Token 可能散落在配置仓库、CI 脚本、甚至日志平台里攻击路径远远比想象的宽泛。所以这篇文章反复强调的核心观点是不要为了临时便利授予长期的高危权限nodes/proxy尤其要当成炸弹来处理。2. 把风险链路拆开看nodes/proxy 如何一步步变成 RCE2.1 nodes/proxy 权限的本质一条通往 kubelet 的代理通道要理解这个风险必须先理解 Kubernetes 的 node 资源和 proxy 子资源到底做了什么。nodes是 Kubernetes 的核心资源对象代表集群里的物理节点或虚拟机节点。正常情况下管理节点信息需要get、list、watch这类资源权限而nodes/proxy则是一个更特殊的操作入口。Kubernetes 的 API 聚合层提供了一个代理机制资源路径为api/v1/nodes/{nodeName}/proxy/{path}API Server 会把{path}部分原封不动地代理给节点的 kubelet 端口。举例来说如果某个用户有nodes/proxy权限请求https://api.example.com/api/v1/nodes/node-1/proxy/metrics实际上等于请求了https://node-1:10250/metrics而 kubelet 会认为这个请求来自 API Server 的代理从而放行对端口的访问。从设计意图上讲这个机制本身是为了解决节点级 API 访问不便的问题。K8s 官方在调试节点、采集 kubelet 指标、查看节点状态时都推荐使用这个方式比如kubectl get --raw访问节点指标就是通过这个代理通道实现的。但设计意图归设计意图一旦这个权限被授予的对象不是高度可信的集群管理服务而是普通业务服务或者给开发人员的账号风险立刻就凸显出来了。关键点在于API Server 在做nodes/proxy转发时不会对代理目标端口做任何限制。它不会检查你访问的是 kubelet 的/metrics还是/exec不会判断你是在读数据还是在执行命令。只要你持有这个权限就可以通过代理通道发起任意请求到 kubelet 的全部端口。这个特性让我经常用一个并不完全恰当但足够直白的类比nodes/proxy就像消防通道的钥匙它本来就是拿来做紧急维修用的但如果你把这个钥匙扔进公共抽屉那任何人都可以打开消防通道进到机房的核心区域而且监控摄像头很可能还拍不到因为所有人走的都是正常门禁路线。2.2 kubelet 的脆弱端口与可利用面既然nodes/proxy的终点是 kubelet那 kubelet 本身到底留了哪些可利用的接口就得掰开来说清楚。kubelet 作为一个常驻在节点上的管理进程对外暴露了多个 HTTP 端口不同类型端口的安全级别差异非常大。首先是 10250 端口这是 kubelet 的主端口负责接收来自 API Server 的指令包括创建容器、执行命令、读取日志等功能。这个端口如果开启了匿名访问或者仅依赖节点上的本地认证文件配合nodes/proxy代理通道攻击者可以直接调用/run/{namespace}/{pod}/{container}接口在容器内执行任意命令这是一条最直接的 RCE 路径。其次是 10255 端口这是 kubelet 的只读端口通常绑定 0.0.0.0提供/pods、/metrics等只读信息接口。虽然它不像 10250 那样能执行命令但通过这个端口可以拿到节点上所有 Pod 的详细清单、镜像信息、容器状态对攻击者而言这是绝佳的信息收集入口能快速定位高价值目标和内部网络结构。再次是 10248 和 10249 端口分别是健康检查和本地流量统计端口CRI 接口的实际大端口通常 10010也可能暴露。通过这些端口能进一步探测容器运行时信息和网络流量情况。在真实攻击链路中Kubernetes 默认是启用了通过 Webhook 模式把nodes/proxy请求转发给 kubelet 进行授权认证的这意味着如果没有正确配置 kubelet 的--authorization-mode或--authentication-token-webhooktrue那么所有经过nodes/proxy到达 10250 的请求都会被 kubelet 当作可信请求来处理直接免认证执行。默认开启的--anonymous-auth参数更是雪上加霜。2.3 从权限绑定到实际控制完整的攻击路径假设攻击者已经拿到了一个绑定了nodes/proxy权限的服务账号 Token完整攻击路径一般会走这样几步。第一步攻击者通过 Token 访问 API Server调用GET /api/v1/nodes列出集群中所有节点名称和内部 IP。这里其实只需要有nodes资源的 list 权限或者通过其他低权限接口间接收集比如有些环境里nodes列表对认证用户默认可见。第二步攻击者构造带 target 的代理请求。Kubernetes 支持通过nodes/{nodeName}/proxy直接指定节点名也支持通过 URL 参数方式指定 IP。实际攻击中更常见的手段是利用kubectl get --raw或直接 curl 请求 API Server 的代理路径。第三步攻击者命中 kubelet 的主端口。请求https://api.internal.cluster.local/api/v1/nodes/{nodeName}/proxy/run/{namespace}/{pod}/{container}这个接口如果 kubelet 的鉴权模块没有对代理请求做二次校验就会直接在目标容器的命令行环境里创建执行进程。第四步攻击者在目标容器里执行命令。这一步的变种很多有些攻击者执行/bin/sh反弹 Shell有些直接把宿主机的敏感文件读取出来还有些会利用容器挂载的云厂商凭据进一步横向移动。如果容器是以特权模式运行甚至可以直接逃逸到宿主机。这个链路里最关键的一环还是从“有权限”到“请求被 kubelet 接受”这一步。正因为nodes/proxy把 API Server 变成了一座“鉴权代理桥”攻击者不需要接触节点网络也绕过了大部分网络策略的检测逻辑。所以我可以明确地说在 K8s 集群里nodes/proxy权限的无约束授权和把 SSH 私钥发给全员本质上没有任何区别。3. 落地防护从权限治理到运行时检测3.1 先从权限治理下手把不该有的授权揪出来防护nodes/proxy滥用第一步永远是权限治理。但这里说的治理不是笼统的“最小权限原则”而是要有具体可执行的动作和命令。第一步梳理集群里所有涉及nodes/proxy或者system:node、system:nodes相关角色的绑定关系。用kubectl get clusterrolebinding -o jsonpath遍历所有 ClusterRoleBinding把引用到高危 ClusterRole 的 Subject 全部列出来。同时也要检查 RoleBinding因为授权可能发生在命名空间维度。第二步针对每个命中对象判断它是否真的需要这个权限。常见的误用场景包括监控组件用system:node拉取指标、运维脚本用system:node管理节点、给开发人员授予nodes/proxy用于调试 Pod。开发调试需求可以用kubectl port-forward或者临时创建带特权的 Pod 解决完全不需要把节点代理权限交给普通账号。第三步清理的过程中不要直接删除绑定优先采用替换方案。先创建自定义 ClusterRole只授予必需的资源和动作再新建绑定确认业务无影响后删除旧绑定。我习惯用rbac-lookup这个开源工具辅助排查它的输出格式比 kubectl 原生命令直观得多能快速看到某个角色被哪些用户、组、服务账号占用。除了静态排查还要把权限治理纳入到上线审批流程里。建议在 CI/CD 的部署管道中加入一个策略检查步骤凡是新创建的 ClusterRoleBinding 或 RoleBinding 如果引用了system:node、system:nodes、含nodes/proxy规则的角色立即阻断并告警。这样能从源头拦截而不只是事后清理。3.2 技术层面收紧 kubelet 的暴露面权限治理解决的是“谁能用”的问题而 kubelet 自身的加固解决的是“用上了能不能生效”的问题。两个层面互相配合才能把nodes/proxy的杀伤力降下来。kubelet 的关键启动参数需要逐一确认--authentication-token-webhooktrue这个参数定义了 kubelet 在收到请求时会把请求的 Bearer Token 发送给 API Server 做 Webhook 验签。没有开启时kubelet 面对匿名请求直接信以为真。开启后即使请求到达 kubelet也必须通过 API Server 的认证校验。--authorization-modeWebhook仅开启认证还不够kubelet 还需要用 Webhook 模式做鉴权也就是把请求的属性和用户信息发给 API Server 的SubjectAccessReview接口做授权判断。如果不开启这个模式kubelet 默认的 AlwaysAllow 或 Node 模式很容易放大nodes/proxy的效果。--anonymous-authfalse默认情况下 kubelet 允许匿名访问 10250 端口的某些只读接口。关闭匿名访问是必须的否则攻击者甚至不需要 Token 就能探测节点状态。--read-only-port0直接禁用 10255 只读端口。这个端口除了信息泄露没有任何实际业务上的必要性。--protect-kernel-defaultstrue虽然不是直接跟 RCE 相关但开启后可以防止 kubelet 随意修改节点内核参数降低容器逃逸之后的破坏面。网络层面需要配合 NetworkPolicy 或者云厂商的安全组限制对节点 10250 端口的访问来源。常规的集群架构里API Server 到 kubelet 的访问是必需的但除此之外的业务 Pod 不应该能直接访问节点主端口的网络地址。有些团队觉得反正都有 TLS 和 RBAC 保护网络开放无所谓但我在实际排查中见过多次因为某个节点组件配置异常把 10250 暴露到了公网的情况一旦网络层没有隔离后续的纵深防御会变得非常被动。3.3 审计与异常检测提前发现而不是事后补救防护体系里最容易被忽略的就是可观测性。很多集群出了事故排查的时候才发现审计日志没开或者开了但根本没监控关键事件。Kubernetes API Server 的审计日志是发现nodes/proxy滥用的第一手信息。配置审计策略时至少要覆盖nodes/proxy这个子资源的请求记录。审计策略可以按集群规模分档生产环境建议 LevelRequestResponse把请求体和响应体都记录下来方便攻击回溯但全量审计会带来不小的存储压力实际上可以只对nodes/proxy相关请求开启 RequestResponse 级别其他请求保持 Metadata 级别这样兼顾了性能和追溯能力。在检测层面要重点关注请求的特征。正常的nodes/proxy使用场景比较集中主要是监控系统定时拉取 kubelet metrics特征是请求路径集中在/metrics频率相对固定。异常请求的特征则非常明显请求路径中出现run、exec、attach等动词的无论什么来源都需要立刻告警。另外还要关注频繁切换目标节点、短时间内大量请求不同节点 kubelet 端口的模式这类行为大概率是横向探测。为了落实这些检测规则可以参考这些开源方案Falco 的 Kubernetes 审计插件可以实时解析审计日志并触发告警规则KubeEye 内置了不少 RBAC 检查规则Cloud Custodian 也能做周期性的权限扫描。没有预算引入复杂平台的小团队先在日志系统里配置几个关键字告警也比什么都不做强得多审计日志里的nodes/node*/proxy/run或者proxy/exec路径配一个正则告警成本很低但效果立竿见影。4. 复现与排查亲手验证风险比听我说更重要4.1 用 kind 搭建一个最小化验证环境纸面讲再多不如实操跑一遍。我强烈建议你现在就在本地用 kind 搭建一个单节点测试集群然后按下面步骤复现整条攻击链路。你踩过的每一个坑都比看十篇文章印象深刻。# 创建单节点 kind 集群kind 默认会安装本地 kubeadmin 配置 kind create cluster --name rce-demo # 确认集群就绪 kubectl cluster-info kubectl get nodeskind 创建出来的集群kubelet 是运行在宿主机上的 Docker 容器里端口映射和真实集群有细微差异但 RBAC 层面的授权逻辑是完全一致的足够验证nodes/proxy的权限边界。接着要调整 kubelet 的参数。kind 默认的 kubelet 配置相对安全为了复现真实问题我们要模拟一个“没有开启 Webhook 鉴权”的典型场景。# 查看当前 kind 节点的 kubelet 版本 kubectl get node kind-control-plane -o jsonpath{.status.nodeInfo.kubeletVersion} # 通过 docker exec 进入 kind 控制平面容器查看 kubelet 启动参数 docker exec -it rce-demo-control-plane cat /var/lib/kubelet/config.yamlkind 节点上的 kubelet 配置通常位于/var/lib/kubelet/config.yaml。这里面的authentication和authorization字段就是防守的核心。为了演示风险你可以把authentication.anonymous.enabled设置为 trueauthorization.mode设置为 AlwaysAllow然后重启 kubelet 容器。注意以上修改仅限本地测试集群。生产环境千万不要这样调整 kubelet你这是在亲手拆掉安全防线。4.2 创建高危绑定并模拟攻击现在开始创建风险和攻击验证用的服务账号。# 创建命名空间和测试服务账号 kubectl create ns attack-sim kubectl create serviceaccount hacker -n attack-sim # 故意绑定 system:node 角色让服务账号拥有 nodes/proxy 权限 kubectl create clusterrolebinding attack-sim-node \ --clusterrolesystem:node \ --serviceaccountattack-sim:hacker接着获取这个 ServiceAccount 的 Token 并配置 kubectl 上下文。# 获取 token新版本 kubectl 可能需要在 secrets 里读取或者 use KUBECONFIG 方式 kubectl -n attack-sim create token hacker --duration1h # 生成新的 kubeconfig 上下文 kubectl config set-credentials attack-hacker --token输出的token kubectl config set-context attack-ctx --clusterkind-rce-demo --userattack-hacker kubectl config use-context attack-ctx当前上下文验证通过后先用低权限视角看看能做什么。# 尝试列出 pods这里如果 system:node 角色包含 list pods 的权限就能看到 kubectl get pods -A接下来是核心攻击步骤直接通过 API Server 的nodes/proxy路径访问 kubelet 的 headers 和版本信息。# 访问 kubelet 的配置接口注意节点名换成你的 kind 节点名 kubectl get --raw /api/v1/nodes/kind-control-plane/proxy/configz | head -50 # 尝试调用 kubelet 的 run 接口在 default 命名空间的某 Pod 容器内执行命令 # 先列一下 default 命名空间的 Pod 和容器名 kubectl get pods -n default实际上kubectl get --raw可以直接请求 kubelet 的 exec 或 attach 接口这些接口在 kubelet 的 HTTP handler 里都有对应路径。Kubelet 的 exec 路径格式通常为/run/{namespace}/{pod}/{container}你可以用 curl 模拟以下请求# 用一个轻量的 shell 命令 curl 走 API Server 代理到 kubelet curl -k -X POST \ -H Authorization: Bearer token \ https://127.0.0.1:6443/api/v1/nodes/kind-control-plane/proxy/run/default/nginx/nginx?cmdidcmd如果你的 kubelet 配置允许上面这条命令会直接返回容器内id命令的结果比如uid0(root) gid0(root)。如果返回 403 或者 401说明 kubelet 的 Webhook 鉴权是生效的此时nodes/proxy的实际可用性就被压缩了很多。4.3 加固后再验证配置差异带来的显著变化现在把 kubelet 的安全参数恢复成默认并加严再跑一次同样的请求你会发现链路被拦得死死的。具体步骤是在/var/lib/kubelet/config.yaml中确认以下设置authentication: anonymous: enabled: false webhook: enabled: true authorization: mode: Webhook重启 kind 控制平面容器里的 kubelet 服务kind 节点本身是 systemd 管理的可能需要用docker exec执行systemctl restart kubelet有些镜像里直接重启容器更快。再次发起同样的代理 exec 请求此时 kubelet 会收到请求但因为没有匿名访问能力请求中携带的 Bearer Token 又来自并非集群节点的服务账号kubelet 会将其发送到 API Server 执行 SubjectAccessReview结果是没有权限访问 kubelet 资源返回 403。这个实验能直观地说明即使nodes/proxy权限落在恶意账号手里只要 kubelet 自身做了严格鉴权RCE 也难以执行成功。通过这个对比实验你会发现一个重要的防护认知云原生的安全永远不是单点防御而是层层设防。nodes/proxy权限治理是第一层kubelet 加固是第二层网络隔离是第三层审计检测是第四层。每一层都有它独立的价值缺一层就少一分纵深。5. 常见问题与踩坑记录5.1 实战中频繁踩到的五个坑我在做集群安全加固和应急响应时反复踩过一些坑这些细节常规文档里很少会写这里整理出来给你排雷。第一个坑是想当然地认为nodes/proxy只存在于system:node角色里。实际上很多自定义 ClusterRole 也包含了nodes/proxy规则而且团队文化越松散的集群这种自定义角色越泛滥。排查时不能只查内置角色绑定要全文检索所有 ClusterRole 的 rules。第二个坑是审计日志只记录了请求但没记录响应体导致攻击发生后无法判断攻击者到底执行了哪些命令。在审计策略里对nodes/proxy请求开启 RequestResponse 级别非常重要能帮助应急响应时回答“攻击者做了什么”这个核心问题。第三个坑是修改 kubelet 参数后没有确认 kubelet 成功加载了新配置。很多发行版的 kubelet 以 systemd 管理改完 config.yaml 后需要重启服务有些甚至需要删除缓存的 kubelet.conf 证书才能彻底生效。如果你没有验证kubectl get --raw /api/v1/nodes/节点名/proxy/configz里显示的实际运行参数很可能你以为加固了实际上没有。第四个坑是只知道查 ClusterRoleBinding忽略了 RoleBinding。RoleBinding 虽然只在单一命名空间内生效但nodes是集群级资源命名空间内绑定了nodes/proxy权限一样能访问所有节点。有些运维把所有权限都建模为 RoleBinding 注入到命名空间排查时如果遗漏了这一层监控告警永远无法覆盖所有高风险授权。第五个坑是网络策略只挡了南北向没挡东西向。我在测试环境模拟攻击时发现即使 kubelet 配置了 Webhook 鉴权如果攻击者已经进入集群内部网络仍然可以绕过 API Server 直接访问节点的 10250 端口。所以网络策略要同时覆盖繁忙的网络平面最好做到业务命名空间默认禁止访问节点网段。5.2 一份可以直接抄的防护清单结合上面的所有内容这里给出一份我在生产环境落地时使用的检查清单按顺序执行基本能帮你把nodes/proxy相关风险收敛到位。检查项操作命令目标状态高危角色绑定排查kubectl get clusterrolebinding -A -o jsonpath{range .items[?(.roleRef.namesystem:node)]}{.metadata.name}{ - }{.subjects}{\n}{end}无业务相关 Subject自定义角色规则扫描kubectl get clusterrole -A -o jsonpath{range .items[?(.rules[].resources[*]nodes)]}{.metadata.name}{\n}{end}仅限可信系统角色kubelet 匿名访问状态kubectl get --raw /api/v1/nodes/{node}/proxy/configzanonymous 为 falsekubelet Webhook 鉴权同上接口检查 authorization 部分mode 为 Webhook审计策略覆盖范围检查 kube-apiserver 启动参数中的审计策略文件nodes/proxy请求记录含 ResponseNetworkPolicy 规则kubectl get netpol -A禁止业务网段访问节点 10250关于审计保留周期建议至少保留 180 天并定期把日志归档到对象存储中。有一次我在处理一个长达半年的渗透测试攻击痕迹时就是因为日志保留期过短导致关键攻击路径无法完整重建这个教训非常深刻。另外补充一点针对nodes/proxy误授权的监控告警配置不要只局限在审计日志的路径关键字上。要同时对 API Server 的 metrics 中添加apiserver_request_total的维度分析观察resourcenodes、subresourceproxy的请求量变化趋势这是从全局视角发现异常访问的有效补充手段。6. 再说说我在实战中体会最深的事整套方案落地过一遍之后我个人的体会是nodes/proxy权限只是 Kubernetes 权限体系里一个极其典型的例子但它暴露出来的问题其实是集群管理过程中普遍存在的授权惰性。很多人在配置权限时脑子里想的永远是“现在能不能跑通”而不是“未来这个权限如果被恶意使用会怎样”。我之前在一家企业的集群里处理过一个实际案例某团队为了实现一个自研的节点维护工具拷贝了网上的一段 RBAC 配置直接给服务账号绑定了system:node。后来这个服务账号的 Token 意外泄露到了公开代码仓库里虽然及时清理了但每当想起那段时间集群曾经暴露在那么大的风险之下后背还是会发凉。好在那次没有发生实际攻击但如果真的被利用攻击者可以通过nodes/proxy在任意节点上执行命令整个集群的性质就变成了一座不设防的矿池。所以如果你现在正在规划或执行集群安全加固我建议把nodes/proxy的风险排查列为第一优先级的动作先跑一遍高危角色绑定扫描确认没有遗漏再逐步收紧 kubelet 的姿态。安全不是一锤子买卖权限治理需要持续进行。每隔一个季度做一次 RBAC 审计确认新增的角色绑定都经过了审批才能让你的集群始终处于可控的状态。希望这篇文章能帮你少走一些弯路把该堵的口子及时堵上。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。