DockerSwarm集群取证
当我们成功仿真出 Docker Swarm 集群的各节点时,往往需要重新配置节点 IP 并恢复 Raft 仲裁,才能让集群重新跑起来,进而提取 service、secret、镜像等关键信息。与 PVE 不同,Swarm 的全部集群状态(节点、服务、密钥、网络)都保存在 manager 节点的 /var/lib/docker/swarm/ 下,拿到 manager 就等于拿到整个集群。
Docker Swarm 基础知识
什么是 Docker Swarm
Docker Swarm 是 Docker 内置的容器编排集群模式(SwarmKit),由 manager 和 worker 两类节点组成:
- manager:负责集群管理与调度,彼此之间通过 Raft 一致性协议 同步集群状态(service 定义、secret、config、网络等)。Raft 状态落盘在
/var/lib/docker/swarm/raft/,只有 manager 才有。通常部署奇数个 manager(3/5 个)以保证仲裁。 - worker:只负责运行容器,本地保存任务状态在
/var/lib/docker/swarm/worker/,不持有 Raft 数据。 - 节点间通信使用 双向 TLS(mTLS),每个节点持有由集群根 CA 签发的证书(
/var/lib/docker/swarm/certificates/),证书默认 90 天轮换一次。 - 容器间跨节点通信走 overlay 网络(VXLAN,UDP 4789),可选 IPsec 加密。
取证要点速览
| 类别 | 关键位置 |
|---|---|
| 集群总状态 | /var/lib/docker/swarm/(manager 含 raft/,worker 仅 worker/) |
| Raft 日志(WAL) | /var/lib/docker/swarm/raft/wal-v3-encrypted/ 或 raft/wal/ |
| Raft 快照 | /var/lib/docker/swarm/raft/snap-v3-encrypted/ 或 raft/snap/ |
| Raft 状态 | /var/lib/docker/swarm/raft/raft-state.json |
| 节点本地状态 | /var/lib/docker/swarm/docker-state.json、state.json |
| 节点证书 | /var/lib/docker/swarm/certificates/(swarm-root-ca.crt、swarm-node.crt、swarm-node.key) |
| worker 任务库 | /var/lib/docker/swarm/worker/tasks.db(BoltDB) |
| Docker 主配置 | /etc/docker/daemon.json |
| 镜像与容器 | /var/lib/docker/image/、/var/lib/docker/containers/ |
| 数据卷 | /var/lib/docker/volumes/<卷名>/_data/ |
| 容器日志 | /var/lib/docker/containers/<id>/<id>-json.log |
| daemon 日志 | journalctl -u docker.service(或 /var/log/syslog) |
离线取证技巧:Raft 数据与 autolock
Raft 日志(WAL)与快照中的记录是 protobuf 编码 的,不能直接 cat 阅读,但可以先用 strings 粗筛出 service 名、镜像名、网络名等明文字段:
strings /var/lib/docker/swarm/raft/wal-v3-encrypted/*.log | less # 未加密时是 wal/*.log
strings /var/lib/docker/swarm/raft/snap-v3-encrypted/* | less
manager 在线时,SwarmKit 自带的控制套接字 /var/run/docker/swarm/control.sock 可被 swarmctl(docker/swarmkit 项目)连接,用于导出节点/服务/密钥清单。
autolock(集群锁)是离线取证的最大障碍:若开启 autolock,Raft 日志与快照会被加密(目录名即 *-encrypted),manager 重启后必须输入 解锁密钥(unlock key) 才能载入集群状态。解锁密钥不落盘,只在开启时通过 docker swarm unlock-key 显示一次。取证时注意:
- 优先在内存镜像、shell 历史、运维文档、密码管理器中搜索该密钥
- 在线仿真时若 daemon 停在等待解锁状态,执行
docker swarm unlock并粘贴密钥 - 密钥丢失则该 manager 的 Raft 数据无法解密,只能依靠其他在线 manager 或 worker 的任务残留重建现场
docker swarm unlock # 粘贴解锁密钥(仿真恢复时第一步)
docker swarm unlock-key # 在线查看/确认当前解锁密钥
仿真环境网络配置
启动网卡并配置 IP
仿真出的节点网卡可能未启用,且 IP 往往与原集群不一致,需手动配置:
ip link set ens33 up
dhclient ens33 # 或按原集群规划手动指定:
ip addr add 192.168.100.11/24 dev ens33
Swarm 节点间必须互通以下端口,防火墙/安全组需放行:
2377/tcp—— 集群管理(Raft 通信)7946/tcp、7946/udp—— 节点发现(gossip)4789/udp—— overlay 网络数据面(VXLAN)IP protocol 50 (ESP)—— 仅 overlay 开启加密时需要
恢复集群运行
仿真题中常见的情形是只有一台 manager 的镜像被导出,或多数 manager 失效导致 Raft 无法选主,集群 API(docker node ls 等)全部报 The swarm does not have a leader。在幸存的 manager 上用 force-new-cluster 强制以本地 Raft 数据重建单节点集群,原有 service/secret/config 定义会保留:
systemctl start docker
docker swarm init --force-new-cluster --advertise-addr 192.168.100.11 # 保留 Raft 状态重建集群
docker node ls # 验证自己成为 Leader
docker swarm join-token worker # 重新生成 worker 加入令牌
其他 worker 节点 IP 变化后,用新令牌重新加入:
docker swarm leave # 在 worker 上先脱离失效的旧集群
docker swarm join --token <新令牌> 192.168.100.11:2377
若节点证书因仿真日期漂移而过期(证书默认有效期 90 天),daemon 日志会出现 certificate has expired,此时需校准时间或轮换 CA(见时钟偏差一节)。
系统取证
基本系统信息
Docker 与 Swarm 版本
docker version # client/server 版本、API 版本
docker info # 最重要:Swarm 段给出 NodeID、是否 manager、Raft 参数、
# Managers/Nodes 数量、CA 轮换计数、autolock 状态
cat /etc/os-release
uname -a
docker info 输出中 Swarm 段的关键字段:
Is Manager: true+Managers: 3—— 确认本机角色与管理面规模Raft: Snapshot Interval / Heartbeat Tick / Election Tick—— Raft 调参痕迹CA Config: Expiry Duration / Force Rotate—— 证书有效期与 CA 被强制轮换的次数Encrypted Raft Log: true—— 即开启了 autolock
主机名与节点标识
hostnamectl
cat /etc/hostname
cat /etc/hosts # 常记录各节点 IP/主机名映射
cat /var/lib/docker/swarm/docker-state.json # 本机 NodeID、JoinToken 等
时间服务器地址
cat /etc/chrony.conf # 或 /etc/chrony/chrony.conf,server/pool 行即时间源
cat /etc/systemd/timesyncd.conf # systemd-timesyncd 环境
timedatectl # 时区与同步状态
chronyc sources # chrony 在线状态
时钟偏差
Swarm 对时钟敏感:Raft 选主依赖心跳超时,节点证书有明确的 NotBefore/NotAfter。仿真出来的多台节点时钟不一致,会导致选主失败或证书校验报错。处理流程:
# 1. 关闭自动同步,防止 NTP 与手动设置打架
timedatectl set-ntp false
systemctl stop chronyd 2>/dev/null; systemctl stop systemd-timesyncd 2>/dev/null
# 2. 手动统一时间(每台节点执行,保持一致)
timedatectl set-timezone Asia/Shanghai
timedatectl set-time "2024-01-01 12:00:00"
hwclock --systohc # 写入硬件时钟
# 3. 逐台确认时间一致
date; timedatectl
# 4. 检查节点证书有效期是否覆盖当前时间
openssl x509 -in /var/lib/docker/swarm/certificates/swarm-node.crt -noout -dates -subject
openssl x509 -in /var/lib/docker/swarm/certificates/swarm-root-ca.crt -noout -dates
# 5. 时间一致后重启 docker 恢复集群
systemctl restart docker
证书因时间漂移被判过期且无法回拨时间时,可在 Leader 上轮换 CA 重新签发:docker swarm ca --rotate(会触发全集群证书重签)。
集群信息
节点列表与角色
docker node ls # 节点 ID、主机名、Status、Availability、Manager Status(Leader/Reachable)
docker node inspect <节点> --pretty # 节点详情:IP、标签、平台、加入时间
docker node inspect <节点> | jq '.[0].ManagerStatus' # 是否 Leader、Raft 可达性
Raft 状态
docker node inspect self --pretty | grep -A3 "Manager Status"
cat /var/lib/docker/swarm/raft/raft-state.json # 本地 Raft 硬状态(任期/投票,JSON)
ls -lh /var/lib/docker/swarm/raft/ # wal/ 或 wal-v3-encrypted/ 日志目录
raft-state.json 是明文 JSON,离线也能读;WAL 与快照为 protobuf(autolock 时加密),在线状态用 docker node ls/docker info 反映,离线可用 strings 粗筛内容。
集群证书与身份
ls /var/lib/docker/swarm/certificates/
# swarm-root-ca.crt 集群根 CA(所有节点互信的基础)
# swarm-node.crt 本节点证书:CN=NodeID,OU=swarm-manager 或 swarm-worker,O=Swarm 集群 ID
# swarm-node.key 本节点私钥(高价值,拿到可伪造节点身份)
openssl x509 -in /var/lib/docker/swarm/certificates/swarm-node.crt -noout -subject -issuer -dates
docker swarm join-token worker # worker 加入令牌(含 CA 指纹与 manager 地址)
docker swarm join-token manager # manager 加入令牌(泄露后可伪造 manager,高敏感)
工作负载取证
Service 与 Stack
docker service ls # 全部 service:副本数、镜像、端口发布
docker service inspect <svc> --pretty # 完整定义:镜像、环境变量、挂载、网络、约束
docker service ps <svc> # 各 task 落在哪个节点、当前/期望状态、失败原因
docker stack ls # stack 列表
docker stack services <stack> # stack 内 service 列表
docker stack ps <stack> # stack 全部 task 分布
docker stack config <stack> # 还原出部署时的 compose 定义(需在 manager 在线时)
离线时 service 定义只能从 Raft 数据中提取(strings 粗筛或 swarmctl),因此仿真恢复 manager 是第一优先级。
容器取证
docker ps -a # 本节点全部容器(含已退出,task 完成后容器默认保留)
docker inspect <容器> # 挂载、环境变量、启动命令、网络、退出码
docker diff <容器> # 容器内文件系统相对镜像的增删改(快速定位落地文件)
docker logs <容器> # 单容器标准输出日志
docker export <容器> -o /导出目录/容器.tar # 导出容器文件系统做静态分析
Secret 与 Config
docker secret ls # 全部 secret 名称与创建时间
docker secret inspect <secret> # 只有元数据,value 永不回显(设计如此)
docker config ls
docker config inspect <config> # config 未加密,Spec.Data 字段为 base64 明文,可直接解码
- secret:密文保存在加密的 Raft 日志中,运行时以 tmpfs 形式注入容器
/run/secrets/<名称>。提取途径:在运行中的容器内cat /run/secrets/<名称>,或docker exec <容器> cat /run/secrets/<名称>;容器已退出则导出文件系统后查看。 - config:明文存储,
docker config inspect <config> --format '{{.Spec.Data}}' | base64 -d直接还原。
镜像导出
docker image ls # 本节点全部镜像(含来源仓库与 tag)
docker save -o /导出目录/<镜像名>.tar <镜像:tag> # 导出镜像做离线漏洞/恶意文件扫描
docker history <镜像:tag> # 构建层历史,常泄露构建期执行的命令
存储取证
/var/lib/docker 目录结构
| 路径 | 内容 |
|---|---|
/var/lib/docker/swarm/ | Swarm 集群状态(见速览表) |
/var/lib/docker/image/overlay2/ | 镜像元数据(imagedb/、layerdb/) |
/var/lib/docker/overlay2/ | 镜像与容器的分层文件系统 |
/var/lib/docker/containers/<id>/ | 容器配置(config.v2.json、hostconfig.json)与 json 日志 |
/var/lib/docker/volumes/<卷名>/_data/ | 数据卷真实内容(重点取证对象) |
/var/lib/docker/network/ | 网络分配状态(files/local-kv.db) |
数据卷取证
docker volume ls # 全部卷
docker volume inspect <卷名> # 挂载点、驱动、创建时间
ls -l /var/lib/docker/volumes/<卷名>/_data/ # 直接读取卷内容(无需容器在线)
cp -a /var/lib/docker/volumes/<卷名>/_data /导出目录/ # 打包带走
overlay 网络与加密
docker network ls # 注意 ingress 与 docker_gwbridge 两个内置网络
docker network inspect <网络> # 子网、网关、是否 encrypted、挂载的容器
docker network inspect输出中"com.docker.network.driver.overlay.vxlanid_list"与各 service 的 VIP 可用于还原流量归属- 创建时带
--opt encrypted的 overlay 走 IPsec 加密,密钥由 Raft/gossip 分发,抓包只能看到 ESP 密文 - 网络侧取证命令:
iptables -t nat -L -n(ingress 负载均衡规则)、bridge fdb show(VXLAN FDB)
用户与权限取证
本地访问控制
纯 Swarm 没有集群级用户体系(那是 UCP 的能力),能连上 docker socket 就等于集群管理员,取证关注本地账户:
cat /etc/group | grep docker # docker 组成员:root 等价权限
getent passwd # 系统账户列表
ls -l /var/run/docker.sock # socket 权限
cat /etc/docker/daemon.json # 是否暴露 TCP 2375/2376(远程 API,重大风险点)
grep -r "tcp://" /etc/systemd/system/docker.service.d/ 2>/dev/null # daemon 启动参数覆盖
节点身份与令牌
如「集群证书与身份」一节:节点身份即 swarm-node.crt 中的 NodeID 与 OU 角色;join-token 与节点私钥泄露等价于身份泄露。shell 历史中常见的敏感操作:
grep -i "swarm\|docker" /root/.bash_history
grep -ri "unlock\|join-token" /home/*/.bash_history /root/.bash_history 2>/dev/null
日志取证
集群工作负载日志(重点)
docker service logs <svc> # 聚合该 service 全部 task 的日志(需 manager 在线)
docker service logs --raw <svc> # 不带 task 前缀的原始输出,便于导出
docker service logs --since 24h <svc>
docker logs <容器> # 单容器日志(worker 节点本地即可执行)
cat /var/lib/docker/containers/<id>/<id>-json.log # 离线直接读容器日志文件
daemon 与系统日志
journalctl -u docker.service --no-pager -e # docker daemon 日志:选主失败、证书错误、解锁提示都在此
journalctl -u containerd.service
cat /var/log/syslog | grep dockerd # 非 systemd 或 syslog 环境
cat /var/log/auth.log # SSH/PAM 登录记录
cat /var/log/faillog 2>/dev/null; last -F # 登录历史
daemon 日志中的高价值事件:
raft: ... became leader/lost leader—— 选主与脑裂时间线certificate相关错误 —— 证书过期/伪造节点尝试加入Waiting for the swarm to be unlocked—— 证明开启了 autolocknode join/node leave—— 节点变动审计
任务执行审计
Swarm 没有独立的操作审计日志,操作痕迹主要靠三处交叉还原:
docker service ps <svc> --no-trunc—— task 的创建/失败/终止时间线与报错journalctl -u docker.service—— API 层面的 service 创建/更新/删除记录/var/lib/docker/swarm/raft/的 WAL 时间戳 —— 集群状态每次变更的落盘时间