Kubernetes 安全初探
一句话结论:K8s 安全的认知框架是「中心辐射型通信模型 + 三层攻击面」——所有组件通信都经过 apiserver(6443),每个通信端口反过来就是一个攻击面(apiserver 未授权/kubelet 10250/etcd 2379/Dashboard/kubeconfig);拿到 Pod shell 后的标准动作是「确认环境→读 SA token→SelfSubjectRulesReview 枚举权限→按 secrets/pods/nodes-proxy/clusterrolebindings 四类权限逐条利用→工作负载配置缺陷(privileged/hostPath/hostPID/hostNetwork/危险 capabilities)逃逸到节点→接管集群」。
排版说明:本文为兼容纯文本存储环境做了等价书写——代码示例中的双引号字符串统一改写为单引号,shell 续行反斜杠已合并为单行,十六进制/字节值用文字或前缀 0x 表示。
一、前言与动机
作者打 HackTheBox 的 Fireflow 这台机器,最后环节是一个 k8s 的 RCE,当时一头雾水,靠 writeup 和 AI 帮忙完成,借此机会对 K8s 做初步认知和学习。本文从架构、通信、攻击面、利用链逐层展开。
二、什么,以及为什么
Kubernetes 是 Google 开源的容器编排管理平台,Go 语言开发,K 到 s 之间 8 个字母故简称 k8s。容器场景主要用 Docker,但实际运维中 Docker 存在问题:大量容器手动部署成本高、崩了不会自动重启、单点故障维护成本高;需手动流量控制扩缩容、操作低效;容器 IP 动态变化、服务发现难;服务挂了难以调度到其他机器、缺负载均衡。
k8s 的意义就是解决这些问题、实现自动化运维。它不是 Docker 的替代品:Docker 负责把应用打包成容器,K8s 负责管理成群的 Docker 容器。
三、K8s 架构:控制平面与工作节点
K8s 把一堆服务器组合对外当成一整个大系统,由一个控制平面(Control Plane / Master)管理,控制平面 + 工作机器组成的系统称为集群(Cluster),工作机器称作节点(Node),每个集群至少需要一个工作节点运行 Pod。
Control Plane 控制平面(集群大脑,做全局决策如资源调度,本身也是一个节点),四大组件:
- kube-apiserver:整个集群唯一的通信入口,Kubernetes API 服务器、控制平面的前端。暴露 API 来定义/部署/管理容器生命周期,kubectl、各内部组件、worker 节点全部通过它通信;认证鉴权、校验请求在这里进行;也是唯一可读写 etcd 的组件。可部署多实例扩缩。
- etcd:集群唯一真实数据源,强一致分布式 KV 数据库,承担所有集群数据。
- kube-scheduler:调度器,监视新创建未指定节点的 Pod,结合因素与策略选合适节点运行。
- kube-controller-manager:控制器管理器,每个控制器逻辑上是独立进程但编译到同一可执行文件同进程运行;通过控制循环持续 watch apiserver、对比期望状态与实际状态、不一致就行动。例:NodeController 监视 Worker 节点状态,节点失联则检测 NotReady,容忍时间后驱逐该节点所有 Pod、告诉 apiserver 删除这些对象,被删 Pod 由调度器重到其他健康节点重建——这就是 K8s 节点故障自愈能力的来源。
Worker 工作节点(执行者,维护运行的 Pod)。Pod 是 K8s 最小调度单元,一个 Pod 可封装一个或多个紧密耦合的容器;K8s 不直接管理容器,而是管理 Pod。每个节点运行:
- kubelet:节点代理,在每个节点上运行,保证容器都运行在 Pod 中。流程:监听 PodSpec → 调用容器引擎(docker/containerd)创建/启停 Pod → 上报本节点 Pod 真实状态回 apiserver 写入 etcd。
- kube-proxy:节点上的网络代理,维护 Service 网络规则(iptables/ipvs),实现 Service 负载均衡、集群内部服务访问。
此外工作节点往往还有容器运行时(负责真正拉镜像/创建/启停容器)。CRI(Container Runtime Interface)是 kubelet 和运行时之间的 gRPC 接口,kubelet 是 CRI 客户端,只要实现 CRI 就能对接,运行时可插拔。
四、K8s 通信与端口:每条路径都是攻击面
K8s 采用中心辐射型(Hub-and-Spoke)API 模式:所有从节点/Pod 发出的 API 调用都终止于 apiserver,其它控制面组件不对外暴露远程服务。通信均基于 HTTP/HTTPS 的 REST API。默认端口:
| 组件 | 端口 | 说明 |
|---|---|---|
| kube-apiserver | 6443 | HTTPS API,集群主入口 |
| etcd | 2379 | 客户端读写端口 |
| kubelet | 10250 | HTTPS API,apiserver 调用它管理节点 Pod |
| kubelet | 10255 | 只读 HTTP API,无需认证即可读节点和 Pod 信息 |
| kube-controller-manager | 10257 | HTTPS 健康检查和指标 |
| kube-scheduler | 10259 | HTTPS 健康检查和指标 |
认证方式三种:客户端证书(x509)、Bearer Token(ServiceAccount Token 与静态 Token)、匿名访问。apiserver 与 kubelet:apiserver 主动调 kubelet 的 10250 执行运维操作——获取 Pod 日志、容器 exec、attach、port-forward,API 路径形如 /api/v1/namespaces/{ns}/pods/{name}/log|/exec。kubectl logs/exec 不是直发目标 Pod,而是先发到 apiserver、再由 apiserver 转发到对应节点的 kubelet、最后由 kubelet 操作运行时。apiserver 还可通过代理路径(/api/v1/nodes/{节点名}/proxy/)直接访问节点/Pod/Service,充当集群内部网关。
apiserver 与 etcd:apiserver 是唯一可读写 etcd 的组件,etcd 监听 2379,apiserver 通过 TLS 双向认证访问它。etcd 存了集群所有数据(Pod/Service/Secret/配置),访问控制极其关键,正常部署只监听本地或仅对 apiserver 开放。
五、Pod 间通信、服务发现与外部访问
网络模型基本原则:每个 Pod 有独立的集群内 IP,Pod 之间可直接 IP 通信、不需 NAT,无论同节点还是跨节点都像两台独立机器。同一 Pod 内多容器共享网络命名空间,拥有相同 IP 与端口空间、通过 localhost 互访(这也是同 Pod 容器不能监听同端口——共享网络栈)。跨节点 Pod 通信由 CNI 插件(Calico/Flannel/Cilium)实现,可能基于路由直转或 VXLAN 隧道封装,对上层透明。
Pod IP 不稳定(重启/扩缩容/节点迁移都会变),为此引入 Service:为一组同标签 Pod 提供稳定虚拟 IP(ClusterIP)和固定 DNS 域名,通过 label selector 确定后端 Pod,后端列表维护在 Endpoints 对象里。负载均衡由各节点 kube-proxy 实现:持续 watch Service/Endpoints 变化,维护 iptables 或 IPVS 规则,访问 ClusterIP 时被拦截改写为某后端 Pod IP。服务发现由 CoreDNS 完成,域名格式 <服务名>.<命名空间>.svc.cluster.local(如 mysql.default.svc.cluster.local)。
ClusterIP 只在集群内部可达,外部无法直接访问 ClusterIP 或 Pod IP——这是 K8s 网络模型的基本边界。从外部访问:NodePort(在每个节点开一个端口 30000-32767,访问任一节点该端口由 kube-proxy 转发到 Service 再到 Pod,缺点是端口有限且直接暴露节点端口);Ingress(七层路由,按域名和 HTTP 路径分发到不同 Service,真正执行转发的是 Ingress Controller 如 Nginx Ingress Controller)。
节点到控制面通信与身份:应用(监控、CI/CD、Operator)需在 Pod 内调 K8s API,需要一个身份,K8s 引入 ServiceAccount。每个命名空间下都有名为 default 的默认 SA,Pod 未指定 SA 时自动使用。Pod 启动时 K8s 会把该 SA 的一个 token 自动挂载到 /var/run/secrets/kubernetes.io/serviceaccount/token,同目录还有 ca.crt(验证 apiserver 身份)与 namespace。Pod 内进程读 token 放入 Authorization 头调用 https://kubernetes.default.svc(apiserver 的集群内 Service 域名),apiserver 验证后按该 SA 绑定的 RBAC 决定允许哪些操作。
六、攻击面分类:从外到内的三层
前面通信的每一条路径反过来就是一个攻击面。按攻击者位置分三类:集群外部(未授权组件端口与暴露的 Dashboard:apiserver 6443、kubelet 10250、etcd 2379 配置不当允许匿名访问);集群内 Pod 中(最核心是 ServiceAccount token,可读 Secret/创建 Pod/提权到集群管理员;Pod 配置缺陷如特权容器/挂载宿主机目录可逃逸到节点);节点上(kubeconfig 文件、组件证书、容器运行时 socket,拿到可横向到其他节点甚至控制面、接管整个集群)。典型路径:外部暴露端点 → 进入某 Pod → 读 SA token → 调 apiserver 横向/提权 → 容器逃逸到节点 → 接管集群。
七、Pod 内信息收集
实际渗透最常见起点不是外部未授权,而是通过应用层漏洞(如 Web RCE)拿到一个 Pod shell。判断是否在 K8s Pod 内:环境变量有 KUBERNETES_SERVICE_HOST、或存在 serviceaccount 目录,基本确定在 Pod 内;进一步看 /proc/1/cgroup 含 docker/containerd/kubepods 说明在容器内;/etc/resolv.conf 的 nameserver 是集群 DNS(常为 10.96.0.10)则进一步确认在集群内。
# 判断是否在 Pod 内
env | grep -i kubernetes
ls -al /var/run/secrets/kubernetes.io/serviceaccount/
收集结果决定下一步:serviceaccount 目录有 token 文件→可用 token 调 apiserver 枚举权限;无 token(设置了 automountServiceAccountToken: false)→跳过 API 利用,直接查容器配置能否逃逸或尝试网络横向。
八、ServiceAccount 与 RBAC:枚举权限
拿到 token 后先定义一组变量复用:
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)
APISERVER=https://kubernetes.default.svc
curl --cacert $CACERT -H 'Authorization: Bearer $TOKEN' $APISERVER/api
测试 token 返三种结果:200 可用;401 无效/过期;403 有效但权限不足(最常见,不代表没利用价值,需先查 SA 到底有哪些权限)。枚举权限最准确的是调 SelfSubjectRulesReview API,返回当前 SA 在指定命名空间下所有权限;若 Pod 里有 kubectl 可直用 kubectl auth can-i --list,加 -n kube-system 查其他命名空间。
RBAC(Role-Based Access Control)核心概念两个:Role(定义一组权限如 list pods/get secrets)和 Binding(把角色赋予某用户/SA)。Role 分命名空间级 Role 和集群级 ClusterRole,对应绑定 RoleBinding/ClusterRoleBinding。cluster-admin 是内置 ClusterRole,拥集群所有权限。枚举完权限后按不同权限进入对应场景:get/list secrets→读 Secret 窃凭据;create pods→创建恶意 Pod 逃逸/提权;get nodes/proxy→通过 apiserver 代理访问 kubelet;create clusterrolebindings→直接绑 cluster-admin 一步提权。权限什么都做不了→转容器逃逸从节点拿更高权限。
九、常见权限利用
get/list secrets:K8s 的 Secret 默认只做了 base64 编码、不是加密,读取后直接解码拿明文。列当前命名空间 Secret,读取具体内容后 echo 编码值 | base64 -d,或装了 jq 直接提取并解码指定字段(如 password)。拿到数据库密码可直连对应服务横向;若读到的是其他 SA 的 token(很多 Secret type 是 kubernetes.io/service-account-token),可切换身份用那个 token 重新枚举权限。
create pods:利用重点不在创建普通 Pod,而在能否在 Pod 定义中挂载 hostPath、用 privileged、指定高权限 SA。先创一个普通测试 Pod 确认权限可用(201 成功,403 无权;一直 Pending 不代表权限问题,可能是资源限制/节点污点)。确认后创带逃逸配置的恶意 Pod(挂载宿主机根目录,exec 进去就能通过挂载点访问宿主机整个文件系统):
apiVersion: v1
kind: Pod
metadata:
name: escape-pod
spec:
containers:
- name: escape
image: alpine
command: [sleep, 3600]
volumeMounts:
- name: host-root
mountPath: /host
volumes:
- name: host-root
hostPath:
path: /
集中多种危险配置的 evil-pod(privileged: true 让容器拥有宿主机所有 capabilities,hostNetwork/hostPID 分别共享宿主机网络与进程命名空间,serviceAccountName 指定高权限 SA 让 Pod 继承其 RBAC):
apiVersion: v1
kind: Pod
metadata:
name: evil-pod
spec:
serviceAccountName: high-priv-sa
hostNetwork: true
hostPID: true
containers:
- name: evil
image: alpine
command: [sleep, 3600]
securityContext:
privileged: true
volumeMounts:
- name: host-root
mountPath: /host
volumes:
- name: host-root
hostPath:
path: /
get nodes/proxy:看起来不如 secrets/pods 直观但实际很关键。能访 nodes/proxy 意味着可通过 apiserver 代理能力访问节点上的 kubelet API、不需直连节点 10250。先列所有节点拿节点名,再通过代理访问指定节点 kubelet、列出该节点上所有 Pod;能列出说明可用。进一步可通过代理读 Pod 日志、获取 Pod 详细信息(含环境变量、挂载卷等敏感内容),甚至 exec 进容器,相当于间接获得 kubelet 访问能力:
curl -k -H 'Authorization: Bearer $TOKEN' --cacert $CACERT https://10.43.0.1:443/api/v1/nodes/fireflow/proxy/pods
# 使用 curl 调用 exec
curl -k --cacert $CACERT -H 'Authorization: Bearer $TOKEN' -N 'https://kubernetes.default.svc/api/v1/nodes/{nodeName}/proxy/exec/{namespace}/{pod}/{container}?command=id&output=1&error=1'
# 使用 websocat 调用 exec
./websocat --insecure --header 'Authorization: Bearer $TOKEN' --protocol v4.channel.k8s.io 'wss://{hostIP}:10250/exec/{namespace}/{pod}/{container}?output=1&error=1&command=id'
create clusterrolebindings:最直接的提权。能创建 ClusterRoleBinding 意味着可把某用户/SA 绑到更高权限 ClusterRole,最常见做法是把当前 SA 绑到 cluster-admin 一步拿到集群管理员权限。201 成功(当前 token 立刻拥有集群所有权限),409 同名绑定已存在、换个名字重试。
十、组件暴露风险(外部打点阶段)
apiserver 未授权:默认监听 6443、正常需证书或 Token。但若管理员把匿名用户 system:anonymous 绑到了高权限角色(如 cluster-admin),或开启了旧版不安全的 8080 HTTP 端口,攻击者可以匿名身份操作集群。探测直接 curl 访问 API 端点看返数据还是 401:返命名空间列表(200)说明存在匿名且有权;401 需认证;403 说明允许匿名但权限不足。
kubelet 未授权:监听 10250(HTTPS)与 10255(只读 HTTP)。若 kubelet 启动时 --anonymous-auth 为 true,可直接通过 10250 列出节点上所有 Pod 并在任意容器执行命令;10255 不需认证就能获取节点/Pod 详细信息。10250 未授权时直接用 URL 格式在指定容器执行命令:curl -k 'https://{node-ip}:10250/exec/{namespace}/{pod}/{container}?command=id&stdin=1&stdout=1&stderr=1'。拿到任意容器 shell 后就可读 SA token、尝试逃逸,转入 Pod 内流程。
etcd 未授权:监听 2379,是集群唯一数据源,存了所有 Secret/配置/资源状态。正常只接受 apiserver 客户端证书、只监听本地。若配置不当允许匿名或暴露到网络,可绕过 apiserver 鉴权直接读取甚至修改集群数据:etcdctl --endpoints=http://{etcd-ip}:2379 get / --prefix --keys-only;最常见利用是读所有 Secret:etcdctl --endpoints=... get /registry/secrets/kube-system/ --prefix。etcd 未授权是危害最大的外部攻击面之一,它绕过了 apiserver 的所有鉴权和审计。
Dashboard 未授权:Web 管理界面,若暴露在公网且配了跳过登录,或用高权限 SA token 登录,登录后就能直接操作集群(通常通过 NodePort 30000+ 或 Ingress 暴露)。能跳过登录直接进管理界面就是未授权;Dashboard 相当于图形化 kubectl,可查所有资源/创建 Pod/读 Secret。
kubeconfig 泄露:kubectl 配置文件,含集群地址、CA 证书和用户凭证(token 或客户端证书)。拿到一个有效 kubeconfig 等于拿到对应用户的集群访问权限,且可离线使用、不需在集群网络内。常被误提交到 GitHub/泄露在 CI/CD 日志/公开网盘。判断有效性:kubectl --kubeconfig=./kubeconfig get nodes,能列出节点说明有效,权限取决于对应用户 RBAC;管理员 kubeconfig 等于完全接管。
十一、工作负载配置风险(容器逃逸)
拿到 Pod shell 后若配置有安全缺陷,可利用它们突破容器隔离拿到宿主机节点 root,即容器逃逸。
privileged(特权容器):securityContext.privileged 为 true 时容器几乎拥有宿主机所有 Linux capabilities、可访问宿主机所有设备,seccomp/AppArmor 也被放宽。判断:cat /proc/self/status | grep Cap(CapEff 接近全 1如 000001ffffffffff),或 fdisk -l 能看到宿主机磁盘。经典逃逸:挂载宿主机根文件系统:
mkdir /tmp/host
mount /dev/sda1 /tmp/host
chroot /tmp/host /bin/bash
# 此时已在宿主机,可写入 SSH 公钥或定时任务
echo 'ssh-rsa AAAA…' 追加到 /root/.ssh/authorized_keys
hostPath:允许把宿主机某目录挂载到容器内。挂的是 / 则容器内可直接读写宿主机所有文件(不需先找设备再挂载)。挂了容器运行时 socket(/var/run/docker.sock 或 /run/containerd/containerd.sock)可直接控制运行时、创建一个新特权容器并挂载宿主机根目录间接逃逸。判断:mount | cat /proc/self/mountinfo、检查 /host /mnt /var/run/docker.sock。运行时 socket 逃逸:docker -H unix:///var/run/docker.sock run -it --privileged -v /:/host alpine chroot /host /bin/bash。
hostPID:hostPID: true 让容器共享宿主机 PID 命名空间,容器内可看到宿主机所有进程(ps aux 能看到 PID 1 是 systemd)。结合 nsenter 可直接进入宿主机 1 号进程命名空间逃逸:nsenter --target 1 --mount --uts --ipc --net --pid -- /bin/bash。
hostNetwork:hostNetwork: true 让容器共享宿主机网络命名空间,可直接看到宿主机所有网卡与监听端口(ip addr/ss -lntup 与宿主机一致)。本身不直接逃逸,但可访问宿主机本地服务(如只听 127.0.0.1 的 etcd)、嗅探流量取凭证,结合其他配置可逃逸。
危险 capabilities:即使非特权容器,额外授予危险 capabilities 也可能逃逸,最典型是 CAP_SYS_ADMIN,允许执行 mount 等系统管理操作,可用来挂载 cgroup 或宿主机设备。查看:cat /proc/self/status | grep Cap 后 capsh --decode=(CapEff 的 hex 值)。包含 CAP_SYS_ADMIN 属高风险,可挂载 cgroup release_agent 或宿主机设备逃逸,具体利用取决于内核版本与其他配置。
十二、网络隔离(NetworkPolicy)
默认 K8s 集群中所有 Pod 全互通、无任何网络隔离。NetworkPolicy 是 K8s 原生的网络隔离机制,采用白名单模式,可限制 Pod 间入站与出站流量。从攻击角度看它影响横向移动能力,从防御角度看是缩小攻击面的重要手段。注意:NetworkPolicy 需 CNI 插件支持才生效,Flannel 默认不支持,Calico/Cilium 支持。判断是否存在 NetworkPolicy:有权时直接列出(curl ... $APISERVER/apis/networking.k8s.io/v1/namespaces/$NAMESPACE/networkpolicies);无权时通过连通性测试间接判断(从一 Pod 访问另一 Pod/Service 特定端口)。连超时但 ping 通→可能被 NetworkPolicy 拒绝;Connection refused→目标端口没开服务不是策略问题;完全 ping 不通→网络层不通。存在 NetworkPolicy 时横向移动需找被策略允许通信的 Pod 作跳板,或利用策略只限 Ingress 未限 Egress 的遗漏;若 CNI 不支持 NetworkPolicy(如默认 Flannel)即使创建了也不生效、横向不受限。