搜索全站

输入至少 2 个字符,查找相关内容。

↑↓ 选择 Enter 打开Esc 关闭

K8s 取证工件地图

Docker 主机上的取证对象是"一台机器 + 若干容器",边界清楚。

最后更新 2026-10-09版本 v2.0维护 DigiForensics查看历史 1

一、概述

Docker 主机上的取证对象是"一台机器 + 若干容器",边界清楚。

Kubernetes 集群把这个边界抹掉了。取证对象从一个容器变成一个由 API Server 调度、etcd 存储、kubelet 执行、数十个控制器共同维护的分布式系统。

同一个 Pod 的完整状态被拆散在五个位置——编排层状态(etcd / API Server 内存)、节点上的实际运行(kubelet)、标准输出(CRI 日志)、凭据与配置(etcd 里的 Secret/ConfigMap)、谁做了什么(审计日志)。

这是 K8s 取证最重要的特殊性:编排系统本身就是最大的攻击面,也是最大的证据库。

攻击面 后果 证据位置
API Server 未授权访问 接管整个集群:读所有 Secret、建特权 Pod、改 RBAC 审计日志、API Server 日志
etcd 明文存储 所有 Secret 明文可读(库口令、镜像仓库凭证、SA token) /var/lib/etcd/member/snap/*.db
Kubelet 10250 端口 可读任意 Pod 数据、也可执行命令 Kubelet 日志
RBAC 配置过宽 一个 ServiceAccount token 泄露即等于泄露其全部权限 审计日志 + RoleBinding

etcd 里存着所有 Secret 的明文这一条必须单独强调。

即便 Secret 在 API 层面是 base64(那不是加密),etcd 里的值也是最终明文。拿到 etcd 快照,就等于拿到了整个集群的所有凭据。

这是单份价值最高的证据,也是取证顺序里排第一的那一项。

而 K8s 也有 Docker 取证没有的优势——审计日志记录了"谁在何时调用了哪个 API、对什么对象、结果如何",是唯一能回答"攻击者用什么身份、做了哪些操作"的证据源。没有它,其余工件只能告诉你"结果是什么"。

在取证流程中的位置:本篇是集群场景的入口与导航,属于采集环节的前置判定层。

它不解析任何结构,只回答:证据散在哪些位置、优先级如何、什么必须抢在前面。容器层的工件见 Docker 取证工件地图 等三篇,本篇不重复;深入分析见 审计日志与 ConfigMap-Secret 与 容器逃逸与运行时痕迹。

涉及的证据形态,以 kubeadm 典型布局为准,不同发行版与部署方式会有差异,以实际检材为准:

路径 内容
/etc/kubernetes/manifests/ 静态 Pod 定义(apiserver/etcd/scheduler/controller-manager 的 YAML)
/etc/kubernetes/pki/ 集群 PKI:CA 证书与私钥、apiserver 证书、kubelet 客户端证书
admin.conf / kubelet.conf / kube-apiserver.yaml kubeconfig(权限都极大);审计策略路径与加密配置 --encryption-provider-config
/var/lib/etcd/member/snap/*.db etcd 快照,含全部集群状态与 Secret 明文
/var/lib/etcd/member/wal/ etcd 预写日志,含最近写入,即使快照过期也有
/var/log/kubernetes/audit.log API Server 审计日志,K8s 取证最重要的一份日志
/var/log/pods/<ns>_<pod>_<uid>/<container>/0.log Pod 日志真实文件(数据源);/var/log/containers/*.log 是软链接
/var/lib/kubelet/pods/<uid>/ Pod 完整目录:容器 rootfs、configz、volume 挂载
/var/lib/kubelet/config.yaml 与 plugins/ CSI/credential-provider 插件(含云厂商凭证交换逻辑)
/var/run/secrets/kubernetes.io/serviceaccount/ 当前 Pod 内的 SA token(控制平面 Pod 也有)
/run/containerd/containerd.sock containerd 的 unix socket,能访问它 ≈ 节点 root

Pod 日志的软链接不能当身份依据:/var/log/pods/... 的真实文件路径里带 namespace、pod 名与 UID,是 Pod 身份的直接证据;而 /var/log/containers/*.log 的命名规则在不同 CRI 下不一致,不能只从软链接名推断身份。

集群里部署过什么、Pod 的完整配置与挂载、etcd 里存着哪些凭据、谁在何时调用了哪些 API、Pod 的标准输出说了什么。

Pod 内部文件系统被改过什么(kubelet/pods/<uid>/ 下的 rootfs 是运行时视图,不是层结构的权威来源);etcd 快照时刻之后的状态(WAL 里可能有)。

"kubectl 查不到"不等于"不存在"——对象可能已删除,删除事件只在审计日志里。

内容边界:所有命令面向已获得合法授权的集群或已制作的只读镜像,目标是发现与固定证据,不提供任何入侵手段。

高危配置的识别写的是如何判定为高危、如何排除误报,不涉及提权或逃逸的实现。

从检材中读到的凭据只做离线分析:识别类型、固定哈希、评估暴露面,不做在线使用。

读者前提:需要 Kubernetes 核心对象(Pod/Deployment/Service/Secret/RBAC)的基本概念、容器运行时基础,以及 Docker 取证工件地图 提供的容器层前提。

如果集群还在线,请先读 2.1 关键路径速查表 与 3.1 取证顺序——这个顺序是本领域最贵的经验之一,颠倒一次就可能永久失去审计日志。

二、核心原理

2.1 关键路径速查表

以下路径以 kubeadm 部署的典型布局为准;不同发行版与部署方式(kubeadm / 二进制 / 云厂商托管 EKS/ACK 等)会有差异,以实际检材为准。

路径 内容 取证价值
/etc/kubernetes/manifests/ 静态 Pod 定义(apiserver/etcd/scheduler/controller-manager 的 YAML) ★★★★★
/etc/kubernetes/pki/ 集群 PKI:ca.crt/ca.key、apiserver 证书与私钥、kubelet 客户端证书 ★★★★★
/etc/kubernetes/admin.conf 集群管理员 kubeconfig(含 client 证书或 token) ★★★★★
/etc/kubernetes/kubelet.conf kubelet 的 kubeconfig(权限极大) ★★★★★
/etc/kubernetes/kube-apiserver.yaml API Server 配置(审计策略路径、加密配置 --encryption-provider-config) ★★★★★
/var/lib/etcd/member/snap/*.db etcd 快照文件,含全部集群状态与 Secret 明文 ★★★★★
/var/lib/etcd/member/wal/ etcd 预写日志(含最近的写入,即使快照过期也有) ★★★★
/var/log/kubernetes/audit.log API Server 审计日志(K8s 取证最重要的一份日志) ★★★★★
/var/log/pods/<ns>_<pod>_<uid>/<container>/N.log Pod 日志原始文件 ★★★★★
/var/log/containers/*.log CRI 日志的软链接(内容与上面相同) ★★★★
/var/lib/kubelet/pods/<uid>/ Pod 完整目录:容器 rootfs、configz、volume 挂载 ★★★★★
/var/lib/kubelet/config.yaml kubelet 配置(静态 Pod 路径、cgroup 驱动、认证授权) ★★★★
/var/lib/kubelet/plugins/ CSI/credential-provider 插件(含云厂商云凭证的交换逻辑) ★★★★
/var/lib/kubelet/cert/、pki/ kubelet 自己的证书与密钥 ★★★★
/var/run/secrets/kubernetes.io/serviceaccount/ 当前 Pod 内的 SA token(控制平面 Pod 也有) ★★★★★
/run/containerd/containerd.sock containerd 的 unix socket(能访问它 ≈ 节点 root) ★★★★★
/var/log/kubelet.log、journalctl -u kubelet kubelet 日志(含 Pod 启停、exec、探针记录) ★★★★
/var/log/syslog、auth.log 宿主机系统与 SSH 日志 ★★★★(见《Linux 服务器取证工件地图》)

⚠️ 三条最容易踩空的:

  1. 托管集群(EKS/ACK 等)的控制平面在云厂商侧,你拿不到 apiserver.yaml、etcd 数据目录、审计日志。必须向委托方明确索取控制平面日志与 etcd 备份,否则报告会有大段"数据不可得"。
  2. 审计日志默认不开启。 API Server 默认 audit-log-path 为空,必须显式配置。没开就是没开,不能推定"没发生"。
  3. /var/log/containers/*.log 是软链接,用 cat 能读,但归档时只 copy 软链接会失效,要 -L 跟随或直接取 /var/log/pods/ 下的真实文件。

2.2 Pod 日志的两条路径(必须讲清)

这是 K8s 日志取证最常被问的一点。

同一份容器输出,在磁盘上有两个位置:

真实文件(数据源):
/var/log/pods/<namespace>_<pod-name>_<pod-uid>/<container-name>/0.log
        │                    │                │              │  └── 轮转序号:0.log, 1.log, 2.log
        │                    │                │              └───── 容器名
        │                    │                └──────────────────── Pod UID(重启会变)
        │                    └───────────────────────────────────── Pod 名
        └────────────────────────────────────────────────────────── 命名空间

CRI 软链接(便于按 pod/容器检索):
/var/log/containers/<pod-name>_<namespace>_<container-name>-<container-id>.log
        └───────┬──────┘└─────┬────┘└──────┬──────┘└─────┬────┘
             Pod 名         命名空间        容器名         容器 ID(前 12 位)
                ↑
        ★ 注意:字段顺序是「Pod名_命名空间」,与 pods 目录的「命名空间_Pod名_UID」相反
        ★ 这里不含 Pod UID,但含容器 ID——容器重启即产生新的软链接
对比项 /var/log/pods/.../N.log /var/log/containers/*.log
性质 真实文件(轮转产生 0.log、1.log…) 符号链接,指向 pods 下的文件
目录/文件命名 <ns>_<pod-name>_<pod-uid>/<ctr>/N.log <pod-name>_<ns>_<ctr>-<container-id>.log
是否含 Pod UID ✅ 含 Pod UID ❌ 不含,但含容器 ID
内容 原始输出 完全相同(同一文件)
轮转历史 ✅ 全部 N.log 都在 ❌ 只有当前那一个
取证价值 ★★★★★ ★★★★
归档方式 cp -p 直接复制真实文件 cp -pL 跟随软链,或 cp -a 只留链接结构

为什么两套命名不对称:pods 目录按 Pod UID 分目录,是为了让"同名 Pod 的每次重启"各自独立留存;containers 目录按 Pod 名 + 容器 ID 建链接,是为了让运维能按名字快速找到当前这一份。

# 验证"同一份数据、两种呈现"
ls -l /var/log/containers/ | head -5
# lrwxrwxrwx ... digiforensics-app_default_web-xm8k2qp4z.log
#   -> /var/log/pods/default_digiforensics-app-6d4f7b2c9d/xm8k2qp4z/web/0.log
#                                  ↑ Pod UID        ↑ 容器 ID  ↑ 容器名
ls -la /var/log/pods/default_digiforensics-app-6d4f7b2c9d/xm8k2qp4z/web/
# -rw-r----- 1 root root 84213 ... 0.log
# -rw-r----- 1 root root 79044 ... 1.log     ← 轮转保留的历史

# 同一 Pod 名有几个 UID 目录 = 重启过几次
ls -d /var/log/pods/*digiforensics-app* | wc -l
# 2     ← 同一 Pod 名对应 2 个不同 UID = 至少重启过一次

# 容器 ID 出现在软链接名里,可直接反查 Pod UID(★ 交叉验证手段)
readlink /var/log/containers/digiforensics-app_default_web-*.log
# /var/log/pods/default_digiforensics-app-6d4f7b2c9d/xm8k2qp4z/web/0.log

⚠️ 两个路径都不能用 tail -f 式的实时跟读做取证,容器运行时日志文件正在被追加。要用 cp -p 复制后再分析,并在报告里说明"这是采集时刻的快照"。

2.3 etcd:整个集群的单点真相

etcd 是 Kubernetes 的唯一持久化存储。

集群里所有对象——包括 Secret 的明文——都在这里。

/var/lib/etcd/
└── member/
    ├── snap/          ★★★ 快照文件 db(v3 存储),取证首选
    │   ├── db
    │   └── *.snap
    ├── wal/           ★★  预写日志,含最近的写入
    │   └── 0000000000000000-0000000000000000-*.wal
    └── snap/*.snap    (旧版本 bolt 格式的快照)

为什么快照是取证首选:

理由 说明
自包含 一个 db 文件里就有某一时点的全部集群状态
不受轮转影响 不像日志那样被覆盖
可离线分析 etcdutl snapshot status 直接读(只读操作)
覆盖 Secret 明文 即使 API 层是 base64,etcd 里也是最终值
# ★ 只读的完整性/状态检查(不改数据,安全)
ETCDCTL_API=3 etcdutl snapshot status /evidence/etcd-snapshot.db
# etcdutl snapshot status /evidence/etcd-snapshot.db
#   Hash:           4a7f2e9b...      ← 逻辑数据摘要(CRC32-Castagnoli),32 位
#   Revision:       184523            ★ etcd MVCC 的 main revision
#   Total Keys:     3471
#   Total Size:     45 MB

# 旧版本集群的兼容命令(etcd 3.3 及更早用 etcdctl)
ETCDCTL_API=3 etcdctl snapshot status /evidence/etcd-snapshot.db -w table

⚠️ Hash 是逻辑数据摘要,不是文件哈希,也不是页结构校验值。

它是 etcd 遍历数据库里全部 bucket 的 key/value 后算出的 CRC32-Castagnoli,宽度只有 32 位。 它既不覆盖快照文件的字节,也不代表"文件被截断"——它只回答"这份数据库的逻辑内容与生成它的那份是否一致"。 需要字节级同一性就用 sha256sum,两者不可互相替代。

另:这个命令同时会跑一遍 bbolt 的文件完整性检查(内部 tx.Check()), 失败时输出的文本是 snapshot file integrity check failed。 missing hash 属于另一个环节——它出现在快照 restore 时检查文件尾部附加的 SHA-256 是否存在,不是 snapshot status 的输出。看到这两个词要分清是哪个阶段报的。

⚠️ Revision 是关键字段但不是时间。

它是 etcd MVCC 存储层的 main revision(主修订号),即这份快照覆盖到的最大版本号。 能与审计日志的 auditID 交叉验证,但本身不含时间戳。要得到时间必须用审计日志。

快照的获取时机决定它反映的时刻——取证时越早抢越接近现场,晚一天可能已经变化很大。

关于加密的重要认知:kube-apiserver.yaml 里的 --encryption-provider-config 指向一个加密配置文件。若配置了静态加密,etcd 里的 Secret 值是密文,需要用对应 provider 解密;若未配置,就是 base64 编码的明文。判断依据是配置文件是否存在,不是猜。 详见第 05 篇。

2.4 kubelet 目录与 Pod 内的凭据

/var/lib/kubelet/pods/<pod-uid>/ 是节点上某个 Pod 的完整目录:

子项 内容 取证价值
<container>/ 容器 rootfs 的软链接(指向 containerd 快照) ★★★★★
configz/ 该 Pod 的完整配置(镜像、命令、挂载、env) ★★★★★
volumes/ Pod 级卷挂载点(emptyDir、ConfigMap、Secret) ★★★★★
etc-hosts、resolv.conf 该 Pod 的 DNS 配置 ★★★
plugins/ CSI 插件的 Pod 级数据 ★★★

ConfigMap / Secret / ServiceAccount token 在宿主机的落地位置(理解"为什么容器里能看到凭据"的关键):

/var/lib/kubelet/pods/<uid>/volumes/
├── kubernetes.io~configmap/<cm-name>/<key>     ← ConfigMap 内容以文件形式落盘
├── kubernetes.io~projected/default/
│   ├── default-token        ★★★ 每个 Pod 都有(除非 automountServiceAccountToken: false)
│   ├── ca.crt  namespace   ← 容器内 SA 三件套
└── kubernetes.io~empty-dir/<name>/              ← emptyDir 数据(Pod 删除即清理)

SA token 泄露 = 该 ServiceAccount 的全部权限泄露。 token 是 JWT,payload 段只是 base64 编码、可直接还原(签名验证需要集群 CA 公钥,但读取信息不需要):

cat /mnt/df/var/lib/kubelet/pods/<uid>/volumes/kubernetes.io~projected/default/default-token
# eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwi...In0.XXXX
echo '<token 第二段>' | base64 -d | python3 -m json.tool
# {
#   "kubernetes.io/serviceaccount/namespace": "digiforensics",
#   "kubernetes.io/serviceaccount/service-account.name": "digiforensics-app",
#   "iat": 1710000000, "exp": 2171000000,        ← exp 极长 = 长效 token(K8s 默认设计)
#   "kid": "dfff456c7bcc79297e3776a35a6c6775e91c91b13"
# }

⚠️ exp 极长(十年)是 K8s 的正常设计,不能据此判定为恶意。 判恶意必须结合"谁在何时从哪拿走了它"——这正是审计日志要回答的问题(第 05 篇)。

2.5 静态 Pod:控制平面的"隐形"部分

kubeadm 部署时,apiserver、etcd、scheduler、controller-manager、CNI 并非普通 Pod,而是由 kubelet 直接从 YAML 文件读取启动的静态 Pod,全部位于 /etc/kubernetes/manifests/。

取证意义(四条,逐条对应下表):

特征 取证含义
不受 RBAC 管辖,kubelet 直接创建 kubectl delete pod 删不掉(会被重建);攻击者改这些文件,kubectl 侧看不到异常
改文件 = 集群级持久化 节点重启后新配置自动生效
在 API 里呈现为"镜像 Pod" 静态 Pod 本体不在 API 中,kubelet 会上报一个同名的 mirror Pod,名字为 <static-pod-name>-<node-name>,带 metadata.ownerReferences 指向 Node,且多一个注解 kubernetes.io/config.mirror。这是判定"某个 Pod 是不是静态 Pod"的唯一可靠 API 特征
审计记录可能不完整 kubelet 直接写文件并上报,创建/修改动作不经 API Server 授权链路,审计日志里往往只看到镜像 Pod 的更新,看不到"谁改了文件"
etcd.yaml 里的 dataDir 与证书路径 判断"数据被搬到哪去了"的关键
文件 mtime 是时间锚点 静态 Pod YAML 极少变动,mtime 变化即异常
目录下出现非标准文件名 ★★ 控制面持久化的首选位置(见第 06 篇)
ls -l --time-style=long-iso /mnt/df/etc/kubernetes/manifests/
# -rw-r----- 1 root root 2841 2023-05-11 09:20:14 kube-apiserver.yaml   ← 与其他文件同期 = 正常
# -rw-r----- 1 root root  892 2024-03-18 03:14:52 audit-shim.yaml      ★ ★★ 非标准文件名
cat /mnt/df/etc/kubernetes/manifests/audit-shim.yaml | head -20
# containers:
# - name: shim
#   image: 198.51.100.24:5000/shim:latest        ★ 外部镜像
#   securityContext: {privileged: true}           ★ ★★ 特权
#   volumeMounts: [{mountPath: /var/log/kubernetes}]  ★ ★★ 读取审计日志

2.6 Kubelet 10250:读端口也是攻击面

端口 服务 风险
10250 Kubelet HTTPS 读写 API 未授权即可读任意 Pod 数据、exec 进容器
10255 Kubelet read-only 端口 未授权可读 Pod 信息与日志
6443 API Server 未授权 = 集群完全失守
2379/2380 etcd 客户端/对等端口 未授权 = 直读所有 Secret

取证含义:若这些端口对外开放(ss -tulpn 可见),攻击者根本不需要进入集群就能拿到数据,日志里也不会有任何记录。"边界暴露"是独立于入侵的严重发现,必须单独查、单独写。

三、操作步骤

3.1 取证顺序(不可颠倒)

etcd 快照和内存都是会被覆盖或丢失的,顺序错了就追不回。

1. ★★ 抢 etcd 快照(member/snap/*.db + wal/)
      理由:全集群状态 + Secret 明文,删了就没了
                ↓
2. ★★ 抢 API Server 审计日志(/var/log/kubernetes/audit.log)
      理由:唯一能回答"谁在何时做了什么"的记录
                ↓
3. ★  内存镜像(若在运行)—— 内存里的 SA token、解密中的密钥
                ↓
4. ★  宿主机活体状态 + Pod 日志 + kubelet 目录
                ↓
5.    /etc/kubernetes/ 配置与 PKI(最稳定,可最后取)
                ↓
6.    全盘镜像 → 离线深度分析

理由说明:etcd 快照是唯一的凭据全量来源且随时可能被 compaction 影响;审计日志是行为证据的唯一来源且有轮转;内存易失;/etc/kubernetes/ 几乎不变,放最后不影响。

3.2 第 1 步:抢 etcd 快照(最高优先级)

# 在 etcd 所在节点(控制平面节点)执行
mkdir -p /evidence/etcd
ls -l --time-style=long-iso /var/lib/etcd/member/snap/
# -rw------- 1 root root 47185920 2024-03-18 06:12:04 db       ★ 快照
# -rw------- 1 root root  8388608 2024-03-18 06:12:04 3e3f...snap

# ★ 只复制,不移动、不压缩(保持原样)
cp -p /var/lib/etcd/member/snap/db /evidence/etcd/etcd-snapshot.db
cp -p /var/lib/etcd/member/wal/*   /evidence/etcd/ 2>/dev/null
sha256sum /evidence/etcd/* | tee /evidence/etcd/hashes.txt

# ★ 立刻做完整性检查(只读,不改数据)
ETCDCTL_API=3 etcdutl snapshot status /evidence/etcd/etcd-snapshot.db
#   Hash: 4a7f2e9b...    Revision: 184523    Total Keys: 3471
# ⚠️ 如果报 "snapshot file integrity check failed",
#    说明快照文件本身在复制过程中被截断或损坏,需重新复制并记录
#    (注意:这条报的是 bbolt 文件结构坏了,不是 Hash 值对不上)

⚠️ **