资讯详情

K8s nodes/proxy权限滥用:一条命令从低权限账号到节点RCE

📅 2026/10/14 7:00:45 | 华诺云谱 👁 阅读
K8s nodes/proxy权限滥用:一条命令从低权限账号到节点RCE
先说结论如果你负责的 K8s 集群里出现过nodes/proxy的create或update权限那你距离集群被拿 shell 可能只差一条命令。nodes/proxy是很多运维同学容易忽略的高危子资源权限它本意是让 API Server 以代理方式访问节点上的 kubelet 端口用于日志、监控等场景。但一旦被滥用攻击者可以通过这一权限直接对节点上的容器执行命令、创建新容器形成一条从“低权限账号”到“节点 RCE”的完整链路。这篇文章我会结合一次模拟攻击的排查过程把权限利用原理、攻击路径、防护策略和自查方法完整拆开讲内容偏工具和实操适合所有正在维护生产集群、又担心 RBAC 越权问题的工程师。1. 风险全景nodes/proxy 到底是什么为什么会成为 RCE 入口1.1 一次不该发生的“低成本”渗透我在一次内部安全演练中专门搭了一套模拟集群给一个测试用的 ServiceAccount 授予了一个“看起来没什么危害”的 ClusterRole规则大致是rules: - apiGroups: [] resources: [nodes/proxy] verbs: [create]当时团队里几个同学都觉得这个权限只是“允许往节点代理发个请求”最多能读到一些指标数据应该不会致命。但实际演练只花了几分钟这个低权限账号就拿到了某台节点上容器的远程 shell还在节点上创建了一个全新容器顺便验证了写文件的能力。整个过程走的是完全合法的 API 路径没有用任何未公开的漏洞也不需要绕过鉴权。问题就出在nodes/proxy这个子资源权限在 RBAC 体系里的特殊地位以及我们对 kubelet API 能力范围的低估。1.2 nodes/proxy 在 RBAC 里的真实含义Kubernetes 的 RBAC 资源模型里节点对象有很多子资源常见的有nodes/proxy对 kubelet 的 HTTP 端点做反向代理nodes/log读取节点上的日志文件nodes/stats读取容器运行时和节点资源统计nodes/metrics读取节点指标其中nodes/proxy是最容易产生误解的一个。从名字上看它像是“借道节点代理访问一些数据”但从 API Server 的实现看它实际上把请求原样转发到了 kubelet 监听的端口。也就是说如果你对某个节点拥有nodes/proxy的create权限相当于获得了以 API Server 身份调用该节点 kubelet 任意写接口的能力。kubelet 不是单纯的“节点信息查询器”它对外暴露的 API 里有/run/{namespace}/{pod}?actioncreate在指定 Pod 内创建容器/exec/{namespace}/{pod}/{container}在容器内执行命令/logs/读取容器日志/attach连接容器标准输入输出/portForward端口转发所以nodes/proxy的授权范围远比“代理”两个字看起来大。1.3 从权限到 RCE 的链条拆解把整条攻击链拆开其实是三步第一跳拿到集群内的一个低权限账号身份。可能是被泄露的 ServiceAccount Token也可能是一个开发环境里继承了大量权限的普通用户。在现实环境中这类账号很多因为它们看起来不会直接影响业务数据。第二跳通过该身份请求 API Server 的 nodes/proxy 端点。请求路径大致是POST /api/v1/nodes/{nodeName}/proxy/...API Server 收到请求后会根据 RBAC 判断该用户是否对nodes/proxy有对应 verb 的权限。由于管理员在 ClusterRole 里写了create请求被放行。第三跳请求被转发到 kubelet 后执行容器创建或命令。kubelet 默认监听在10250端口如果请求路径拼的是/run/...或/exec/...就直接触发了命令执行能力。理解这个链条后你会发现真正的问题不是单一漏洞而是“宽泛的代理权限 kubelet API 的强能力”组合后的必然结果。只要nodes/proxy给了写操作RCE 就是时间问题。2. 攻击路径模拟一个低权限账号如何拿到节点 Shell2.1 模拟环境准备为了验证风险我搭建了一个相对常见的集群环境Kubernetes 版本v1.24证书集群启用 TLSAPI Server 只允许 HTTPSkubelet开启了 Webhook 模式认证和授权但 API Server 到 kubelet 的请求仍然正常一个测试节点node-test-01一个低权限 ServiceAccountsa-attacker先把这个账号创建出来并绑定到模拟场景中的恶意 ClusterRoleapiVersion: v1 kind: ServiceAccount metadata: name: sa-attacker namespace: default --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: cr-nodes-proxy-create rules: - apiGroups: [] resources: [nodes/proxy] verbs: [create] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: crb-nodes-proxy-create roleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cr-nodes-proxy-create subjects: - kind: ServiceAccount name: sa-attacker namespace: default这个账号的权限非常有限没有get pods没有exec没有create pods甚至没有nodes/log只有这一个nodes/proxy的create。2.2 从代理请求到容器内命令执行获取该账号的 Token 后直接在集群外发起请求TOKEN$(kubectl -n default create token sa-attacker --duration1h) curl -k -H Authorization: Bearer $TOKEN \ https://API_SERVER_ADDR:6443/api/v1/nodes/node-test-01/proxy/exec/default/nginx-demo/nginx?commandidcommandhostnamestdinfalsestdouttrue这里请求的是 kubelet 的/exec端点路径经过 API Server 的nodes/proxy转发。返回结果里能看到uid0(root) gid0(root) groups0(root) node-test-01也就是说我已经在没有 Pod 相关任何权限的情况下在目标节点的某个容器里执行了命令。如果想更进一步可以使用/run接口创建一个全新容器。老版本 kubelet 的路径是curl -k -XPOST -H Authorization: Bearer $TOKEN \ https://API_SERVER_ADDR:6443/api/v1/nodes/node-test-01/proxy/run/default/nginx-demo?actioncreatenameattacker虽然不同版本的参数略有差异但核心思路一致所有发往/api/v1/nodes/{nodeName}/proxy/*的请求都会原样到达 kubelet 的对应 handler。攻击者只要不断探测/exec、/run、/portForward这些端点就能在节点上实现命令执行。2.3 为什么 kubelet 的默认能力会放大危害这个攻击能成功除了 RBAC 配错另一个原因是 kubelet 自身默认暴露了大量能力。kubelet 的 10250 端口不是“只读监控端口”它同时提供长连接、容器操作、日志等接口。如果 kubelet 以--anonymous-authtrue或--authorization-modeAlwaysAllow启动那甚至不需要什么权限直接访问 10250 端口就能执行命令。即便很多集群把 kubelet 配置成了 Webhook 模式API Server 通过nodes/proxy转发过来的请求仍然会被 kubelet 当作“来自 API Server 的合法请求”处理因为这套转发链路上的身份已经通过了 API Server 的 RBAC 鉴权kubelet 侧一般会放行。也就是说你在 RBAC 上放过的每一个写权限都可能直接等价于 kubelet 的写接口放行。这也是为什么节点子资源的授权必须慎之又慎。3. 防护落地从准入控制、RBAC 到运行时兜底3.1 最小化 RBAC不再授予 nodes/proxy 写权限最容易落地、效果最明显的方案是重新梳理所有 ClusterRole 和 Role把nodes/proxy的create、update、patch、delete权限全部收回。如果你只是希望让某个组件能读取节点状态完全不需要nodes/proxy。常见监控组件用的是nodes/stats、nodes/metrics日志采集组件用的是nodes/log它们和nodes/proxy是不同的子资源。对确实需要代理能力的系统组件建议遵循如下原则只给get、list、watch不要给写操作限制resourceNames为具体节点名而不是留空在单独命名空间使用单独 ServiceAccount避免权限池化一个相对收敛的 ClusterRole 示例apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: nodes-proxy-readonly rules: - apiGroups: [] resources: [nodes/proxy] verbs: [get, list, watch] resourceNames: [node-test-01]更安全的做法是直接不给任何 subject 绑定nodes/proxy写权限全部改为rules: - apiGroups: [] resources: [nodes/log, nodes/stats, nodes/metrics] verbs: [get, list, watch]这样既能满足绝大多数可观测性需求又能砍掉最危险的攻击面。3.2 kubelet 加固把兜底身份也关掉很多集群只关心 API Server 的 RBAC却忽略了 kubelet 自身的认证授权配置。强烈建议检查每个节点上的 kubelet 配置至少做到以下几点。第一关闭匿名访问。在 kubelet 配置文件的authentication段authentication: anonymous: enabled: false webhook: enabled: true x509: clientCAFile: /etc/kubernetes/pki/ca.crt authorization: mode: Webhook第二确认 kubelet 与 API Server 之间使用证书或 Token 认证并且 API Server 侧配置了 kubelet 客户端证书--kubelet-client-certificate/etc/kubernetes/pki/apiserver-kubelet-client.crt --kubelet-client-key/etc/kubernetes/pki/apiserver-kubelet-client.key这样即使有人通过nodes/proxy发起请求kubelet 侧仍会校验来源身份不会盲目放行。第三如果环境允许可以通过 NetworkPolicy 限制 10250 端口只对 API Server 和运维跳板机开放。虽然这类端口限制不能拦截经 API Server 转发的流量但能挡住直接扫端口攻击属于纵深防御的一部分。3.3 用准入控制策略阻止高危节点子资源权限被创建RBAC 配错往往是人为操作导致靠“每次申请时人工审核”很难完全避免。更可靠的方式是在集群里部署策略引擎从规则层面拒绝任何包含nodes/proxy写操作的角色创建请求。这里以 Kyverno 为例写一个简单但有效的 ClusterPolicyapiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: block-nodes-proxy-write spec: validationFailureAction: Enforce rules: - name: block-create-update-nodes-proxy match: resources: kinds: - rbac.authorization.k8s.io/v1/ClusterRole - rbac.authorization.k8s.io/v1/Role validate: message: Do not grant create/update/patch/delete on nodes/proxy deny: conditions: any: - key: {{ request.object.rules[]?.resources[] || }} operator: Equals value: nodes/proxy如果你更习惯 OPA Gatekeeper也可以写一个禁止 verbs 为create/update/patch/delete且 resources 包含nodes/proxy的 ConstraintTemplate。策略不是摆设它能保证以后再有人想加权限提交就会直接失败。3.4 审计与监控让滥用行为无处遁形防护做得再好也要能看到谁在什么时候做了什么。Kubernetes API Server 的审计日志是这里最关键的数据源。创建一个审计策略文件将nodes/proxy的写操作单独捕获apiVersion: audit.k8s.io/v1 kind: Policy rules: - level: RequestResponse verbs: [create, update, patch, delete] resources: - group: resources: [nodes/proxy] - level: Metadata verbs: [get, list, watch] resources: - group: resources: [nodes/proxy]然后在 API Server 启动参数里挂载这个文件--audit-policy-file/etc/kubernetes/audit-policy.yaml --audit-log-path/var/log/kubernetes/audit/audit.log --audit-log-maxage30 --audit-log-maxbackup10 --audit-log-maxsize200日志收集后最好接一层实时告警。只要出现verbcreate且objectRef.subresourceproxy的请求立刻触发高优级工单。这类请求在一个配置正常的集群里几乎不应该出现出现就说明权限面已经异常。4. 自查与排查怎么确认你的集群是否暴露过风险4.1 快速扫描把集群里所有“危险角色”翻出来如果你不确定自己的集群有没有给过nodes/proxy写权限最快的办法是直接扫描所有 Role 和 ClusterRole。先找出所有在 rules 里出现nodes/proxy的角色kubectl get clusterroles -o json | jq -r .items[] | select(.rules[]? | .resources[]? nodes/proxy) | .metadata.name Role 同理kubectl get roles -A -o json | jq -r .items[] | select(.rules[]? | .resources[]? nodes/proxy) | \(.metadata.namespace)/\(.metadata.name) 拿到名单后再看这些角色绑定了谁kubectl get clusterrolebindings -o json | jq -r .items[] | select(.roleRef.name role-name) | .subjects[]? | \(.kind)/\(.name) 如果你需要更快地判断某个 ServiceAccount 是否危险直接模拟权限检查kubectl auth can-i create nodes/proxy \ --assystem:serviceaccount:default:sa-attacker输出yes就代表该账号具备创建代理请求的能力需要立刻处理。4.2 从审计日志里找攻击痕迹如果集群已经开了审计日志可以通过关键字检索历史请求。在审计日志的 JSON 记录里重点看这几个字段objectRef.resource等于nodesobjectRef.subresource等于proxyverb等于create、update、patchuser.username是哪个账号requestURI是否包含/proxy/exec、/proxy/run用 jq 过滤出所有可疑记录jq select(.objectRef.subresource proxy and .verb create) \ /var/log/kubernetes/audit/audit.log如果看到requestURI里有/exec或run的特征基本可以确认该账号已经尝试过执行命令。需要注意有些攻击者会使用get或list读取 kubelet 暴露的指标、配置等敏感信息这类请求在日志里更容易被淹没。建议把nodes/proxy的所有访问都单独建一个索引至少保留一年。4.3 常见误判与坑我见过不少团队把nodes/log和nodes/proxy混为一谈。实际上nodes/log只能读日志文件危险面相对小nodes/proxy是把整个 kubelet HTTP 端口暴露给用户能力大得多。排查看日志时不要看到nodes/log就当成高危告警也不要因为nodes/proxy只出现一个get就放松警惕——虽然get通常不能直接执行命令但一些旧版本 kubelet 的调试接口可能允许通过get请求携带执行参数务必仔细核对版本行为。另一个常见坑是只检查 ClusterRole忘了 Role。nodes/proxy也可以出现在某个命名空间内的 Role 里而且通过 RoleBinding 绑定到目标命名空间的 ServiceAccount。这种局部权限更容易被忽略但一旦攻击者拿到该命名空间任意 Pod 的控制权就能利用这个本地身份横向移动。所以扫描范围一定要包含roles和rolebindings不要只看全局。5. 落地经验与最后一点提醒5.1 一份可以直接抄的加固清单我整理了一份比较完整的检查清单照着做一遍能覆盖掉大部分nodes/proxy滥用风险确认集群内所有ClusterRole和Role中没有对nodes/proxy授予create、update、patch、delete权限如果某组件确实需要代理能力将resourceNames限定到指定节点且只给只读 verb检查 kubelet 匿名访问已关闭authentication.webhook.enabled和authorization.modeWebhook已配置确认 API Server 使用证书访问 kubelet而不是空凭据部署 Kyverno 或 Gatekeeper阻断高危 RBAC 创建开启审计日志增加对nodes/proxy写操作的实时告警每季度执行一次全量 RBAC 扫描生成权限报告并对比基线按这个清单执行后即使未来有人误配权限攻击者也很难从代理链路拿到节点 shell。5.2 我踩过的一些坑有一次我为了收紧权限直接把某个监控组件的nodes/proxy全部移除结果该组件立刻报错节点指标全部消失。后来查下来发现这个组件竟然还在依赖nodes/proxy读 kubelet 的/metrics端点。正确的做法不是一刀切删除而是先确认组件实际使用的子资源再替换成nodes/metrics或nodes/stats。类似的我还碰到过修改 kubelet 配置后忘记滚动更新节点导致部分节点仍保留匿名访问的情况。这里强烈建议把 kubelet 配置文件纳入 GitOps 管理通过 DaemonSet 或节点初始化工具统一分发避免人工登录节点修改造成的配置漂移。最后再分享一个个人觉得很好用的习惯在集群里准备一个“特权探针”服务它单独拥有一个高权限账号定时尝试用nodes/proxy的写权限发起请求如果请求成功说明加固失效了立刻告警。用攻击者的姿势做日常巡检比任何文档都管用。这篇内容里涉及的攻击路径和防护方案都是我在模拟环境里验证过的真实集群里的版本差异会影响具体指令参数但核心逻辑不会有太大变化。安全加固从来不是一次性的工作希望这份落地经验能帮你少走点弯路。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑