Docker 基础体系 · 第 70/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。
Docker 安全事件响应:隔离容器、保全证据、镜像追溯和恢复
Docker 安全事件响应的目标不是“尽快删掉可疑容器”,而是在不扩大影响的前提下,依次完成四件事:
- 隔离(containment):阻止攻击者继续通过容器访问外部系统或横向移动。
- 保全证据(preservation):保存能够回答“发生了什么、何时发生、影响了什么”的运行态和持久态信息。
- 追溯(attribution and provenance):确定容器实际使用的镜像、构建来源、配置来源和发布链路。
- 恢复(recovery):从可信镜像和可信配置重新部署,并验证旧的持久化数据和凭据没有继续携带风险。
这四个目标之间存在冲突。例如,立即删除容器可以快速止损,却会丢失进程、环境变量、容器文件系统和网络配置;立即重启宿主机可以清理内存中的恶意进程,却会破坏内存和运行态证据。因此,响应流程必须先判断影响范围和紧迫性,再选择“隔离但保留运行态”还是“立即停止并转入主机级取证”。
本文讨论的是 Linux 容器边界内的 Docker Engine 事件响应。容器共享宿主机的 Linux 内核,容器隔离依赖 namespaces、cgroups、Capabilities、Seccomp、LSM 和文件系统隔离。容器并不是虚拟机;如果事件涉及宿主机内核、Docker daemon、/var/run/docker.sock、特权容器或宿主机凭据,应把事件升级为宿主机安全事件,而不能只按“某个容器被入侵”处理。
一、先确定事件边界:容器、daemon 还是宿主机
1. Docker 容器的安全边界是什么
Linux 容器通常由以下机制共同构成:
- namespace:隔离进程、网络、挂载点、用户和 IPC 等视图;
- cgroup:限制和统计 CPU、内存、进程数等资源;
- Capabilities:把传统 root 权限拆分为较小的权限集合;
- Seccomp:限制进程能够调用的系统调用;
- Linux Security Module:例如 AppArmor 或 SELinux;
- overlay 文件系统:把只读镜像层和容器可写层组合成容器看到的根文件系统;
- volume、bind mount、tmpfs:把容器外部的数据或内存文件系统挂载到容器内。
这些机制的作用是降低权限和缩小可见范围,但它们并不消除共享内核这一事实。容器内的 root 通常只是容器 namespace 中的 root,但以下配置会显著扩大风险:
services:
app:
image: example/app@sha256:...
privileged: true
pid: host
network_mode: host
volumes:
- /:/host
- /var/run/docker.sock:/var/run/docker.sock
其中:
privileged: true会大幅放宽设备和 Capability 限制;pid: host允许容器看到宿主机的进程命名空间;network_mode: host使容器直接使用宿主机网络命名空间;- 挂载
/使容器可以访问宿主机文件; - 挂载 Docker socket 通常意味着可以通过 Docker API 创建具有宿主机级影响的容器。
因此,事件响应的第一个问题不是“哪个进程异常”,而是:
可疑容器是否仍处在预期的隔离边界内?
2. 三种影响范围
可以把影响范围分成三个层次。
容器级事件
例如:
- 容器内出现异常进程;
- 应用账号被盗用;
- 容器内文件被修改;
- 容器向外发起异常连接。
此时 Docker 的容器、网络、日志和镜像元数据是主要证据来源。
Docker 平面事件
例如:
- 未授权用户访问 Docker socket;
- daemon API 暴露在不受信任的网络;
- 有人创建了未知容器、网络、volume 或 secret;
- 使用 Docker API 修改了运行配置。
此时需要审查 daemon 日志、API 访问入口、宿主机用户和 socket 权限。仅查看可疑容器不足以解释事件。
宿主机级事件
例如:
- 容器能够访问宿主机根文件系统;
- 出现宿主机 namespace 中的异常进程;
- 内核漏洞或运行时漏洞可能被利用;
- 宿主机上的 SSH、云凭据或其他服务受到影响。
此时 Docker 命令输出只能作为一部分证据。需要按照宿主机入侵响应流程保全磁盘、内存、系统日志、审计日志和云平台日志。
二、响应前的原则:先记录,再改变状态
Docker CLI 通过 Docker daemon 查询和修改对象。命令本身不是“旁观者”:
docker exec会创建新的进程,改变进程列表和审计记录;docker cp读取文件,但可能触发文件访问时间变化,具体取决于文件系统和挂载方式;docker inspect通常是只读查询,但 daemon 和日志系统仍可能产生访问记录;docker stop会发送终止信号并导致进程退出;docker pause会冻结进程,改变运行态;docker network disconnect会改变网络状态;docker rm、docker system prune等操作会破坏证据。
因此,响应人员应先建立一个不可变的响应记录,例如:
export IR_ROOT="/var/ir/$(date -u +%Y%m%dT%H%M%SZ)"
sudo install -d -m 0700 "$IR_ROOT"
sudo date -u +"%Y-%m-%dT%H:%M:%SZ" | sudo tee "$IR_ROOT/started-at.txt"
sudo docker version | sudo tee "$IR_ROOT/docker-version.txt"
sudo docker info | sudo tee "$IR_ROOT/docker-info.txt"
sudo docker ps -a --no-trunc | sudo tee "$IR_ROOT/containers.txt"
sudo docker images --digests --no-trunc | sudo tee "$IR_ROOT/images.txt"
sudo docker network ls | sudo tee "$IR_ROOT/networks.txt"
sudo docker volume ls | sudo tee "$IR_ROOT/volumes.txt"
这里使用 --no-trunc 很重要。默认输出可能截断容器 ID、镜像 ID 或命令,无法作为可靠的关联键。
这些命令的前提是当前账号有权限访问 Docker daemon。对 rootful Docker,常见的 socket 是 /var/run/docker.sock;能够访问该 socket 的用户通常具有接近宿主机 root 的控制能力。因此,响应记录必须保护好,不能把结果写入受可疑容器控制的路径。
如果怀疑 Docker daemon 或宿主机已经被控制,应尽量从受信任的管理通道执行采集,并把输出写入远程或只读证据存储。攻击者可能伪造容器内日志、删除 Docker 对象,甚至修改宿主机上的 Docker 数据目录。
三、隔离容器:暂停、断网和停止分别解决什么问题
“隔离容器”不是一个单一操作。至少需要区分进程隔离和网络隔离。
flowchart LR
A[发现可疑容器] --> B{宿主机是否可能被突破}
B -->|否| C[记录状态与证据]
B -->|是或不确定| D[升级为宿主机事件]
C --> E{是否需要保留运行态}
E -->|需要| F[暂停进程]
E -->|不需要或风险紧迫| G[停止容器]
F --> H[断开 Docker 网络]
G --> H
H --> I[采集容器和镜像证据]
I --> J[轮换凭据并重建]
J --> K[验证恢复]
1. docker pause:保留进程状态,但不等于隔离
sudo docker pause CONTAINER_ID
sudo docker inspect --format \
'{{.Id}} status={{.State.Status}} paused={{.State.Paused}} pid={{.State.Pid}}' \
CONTAINER_ID
Linux 下,Docker 通常通过 freezer 机制冻结容器中的进程。暂停的价值是:
- 保留进程列表、内存映射和容器可写层的当前状态;
- 暂时阻止用户态进程继续执行;
- 为后续采集争取时间。
但暂停不是完整的网络隔离:
- 已建立的 TCP 连接可能仍然存在;
- 内核不会因为用户态进程暂停而自动撤销外部防火墙规则;
- 容器外的宿主机进程、其他容器和网络设备仍可访问相关服务;
- 某些内核级活动不应简单地假设会被容器 pause 完全阻止。
所以,pause 适合“先冻结状态”,不适合单独作为 containment 完成标志。
2. docker network disconnect:阻止通过 Docker 网络继续通信
先查看容器连接了哪些网络:
sudo docker inspect --format '{{json .NetworkSettings.Networks}}' CONTAINER_ID
sudo docker network ls
然后对每个非必要网络执行:
sudo docker network disconnect -f NETWORK_NAME CONTAINER_ID
-f 表示强制断开。断开后,可用以下命令验证:
sudo docker inspect --format '{{json .NetworkSettings.Networks}}' CONTAINER_ID
注意几个边界:
- 容器使用
network_mode: host时,不存在普通的容器网络连接可供这样断开;必须在宿主机防火墙、云安全组或上游网络设备层面阻断。 - 容器使用
network_mode: none时,本来就没有普通网络连接。 - 容器可能通过 bind mount、Docker socket、共享 PID namespace 或宿主机上的其他进程间接影响系统;断网不能解决这些路径。
- 断开网络会改变运行态,可能导致应用写入错误日志、连接重试或数据不一致,所以应先记录网络配置。
如果攻击者正在进行数据外传,网络隔离的优先级可能高于完整采集;这属于“先止损、后保全”的情况。此时必须记录隔离发生的准确时间和执行人。
3. docker stop:终止进程,风险最高但止损最直接
sudo docker stop --time 10 CONTAINER_ID
Docker 通常先向容器的 PID 1 发送 SIGTERM,等待指定时间,再发送 SIGKILL。实际行为还受到容器自身的 signal handling、STOPSIGNAL 和 PID 1 实现影响。
停止容器会丢失或改变:
- 进程运行态;
- 尚未落盘的内存数据;
- 临时文件;
- 当前连接状态;
- 进程树和部分网络状态。
当容器正在访问宿主机、删除数据、读取敏感凭据或持续外传时,应优先停止或在更外层断网。反之,如果风险暂时可控且需要内存、进程和连接信息,应先采集或暂停。
停止后的验证:
sudo docker inspect --format \
'{{.Id}} status={{.State.Status}} exit={{.State.ExitCode}} finished={{.State.FinishedAt}}' \
CONTAINER_ID
Exited 只能说明容器主进程已退出,不代表事件结束。挂载的 volume、宿主机上的恶意进程、已复制出去的凭据和攻击者创建的其他对象仍然存在。
四、保全容器证据:配置、运行态、文件系统和日志
1. 保存完整容器元数据
sudo docker inspect CONTAINER_ID > "$IR_ROOT/container-inspect.json"
sudo docker top CONTAINER_ID > "$IR_ROOT/container-top.txt" 2>&1 || true
sudo docker port CONTAINER_ID > "$IR_ROOT/container-ports.txt" 2>&1 || true
sudo docker diff CONTAINER_ID > "$IR_ROOT/container-diff.txt" 2>&1 || true
这些输出分别回答不同问题:
docker inspect:容器 ID、创建时间、状态、入口命令、环境变量、挂载、Capability、网络和使用的镜像 ID;docker top:容器当前可见的进程;docker port:端口映射;docker diff:相对于镜像层,容器可写层中新增、修改和删除了哪些路径。
docker diff 的状态通常包括:
A:新增;C:修改;D:删除。
它不是完整的文件取证工具。它不提供文件内容的时间线,也不一定揭示挂载 volume 中的变化,因为 volume 不属于容器可写层。
2. 保存容器日志,但不要把日志当作唯一证据
sudo docker logs --timestamps CONTAINER_ID > "$IR_ROOT/container-stdout-stderr.log" 2>&1
docker logs 主要读取容器日志驱动收集的标准输出和标准错误。它的可用性依赖日志驱动:
sudo docker inspect --format \
'{{json .HostConfig.LogConfig}}' CONTAINER_ID
如果应用把日志写入容器内部文件,docker logs 看不到;如果使用 syslog、journald、云日志或第三方驱动,日志可能需要从对应后端采集。还要检查日志轮换和保留策略,因为历史记录可能已经被覆盖。
Docker events 可用于观察 daemon 记录的对象事件:
sudo timeout 60 docker events \
--since '2025-01-01T00:00:00Z' \
--until '2025-01-02T00:00:00Z' \
> "$IR_ROOT/docker-events.txt"
docker events 不是持久化审计日志。事件可能因 daemon 重启、保存周期和日志策略而不可获得,且它不能证明是谁通过哪个身份执行了 API 操作。生产环境应结合:
- systemd journal 或 daemon 日志;
- Linux audit;
- 云平台审计日志;
- Docker socket 访问记录;
- CI/CD 和镜像仓库审计记录。
3. 保存文件系统证据:区分可写层、volume 和 bind mount
先提取挂载信息:
sudo docker inspect --format '{{range .Mounts}}
type={{.Type}} source={{.Source}} destination={{.Destination}} rw={{.RW}}
{{end}}' CONTAINER_ID
常见类型的证据位置不同:
| 挂载类型 | 数据位置 | 取证注意事项 |
|---|---|---|
volume |
Docker 管理的 volume 数据目录 | 不包含在 docker export 中 |
bind |
宿主机指定路径 | 可能直接影响宿主机或其他服务 |
tmpfs |
内存 | 停止容器后通常消失 |
| 无挂载的容器可写层 | overlay 存储驱动管理的目录 | 需要谨慎处理存储驱动内部结构 |
可以使用 docker cp 复制容器内路径:
sudo docker cp CONTAINER_ID:/etc "$IR_ROOT/container-etc"
但它适合快速获取少量文件,不等价于位级取证:
- 复制过程中容器文件可能继续变化;
- 容器路径可能是 volume 或 bind mount;
- 文件元数据、稀疏文件、扩展属性和删除文件的保留情况不能简单假设;
- 从可疑容器内执行
tar会创建新的进程,且容器内工具可能被攻击者替换。
对于需要可靠证据的场景,应在暂停或停止后,对宿主机上的对应 volume 或文件系统进行只读快照、块级镜像或经过验证的归档。不要直接修改 Docker 的 overlay2 内部目录,也不要把“进入容器后打包”当成可信的原始证据。
4. docker export 和 docker commit 的边界
sudo docker export CONTAINER_ID -o "$IR_ROOT/container-rootfs.tar"
sha256sum "$IR_ROOT/container-rootfs.tar" \
| sudo tee "$IR_ROOT/container-rootfs.tar.sha256"
docker export 导出的是容器文件系统视图,但有两个关键限制:
- 不会导出挂载的 volume 内容;
- 它不是镜像的原始层结构,也不保留完整镜像历史。
因此,导出的 tar 可以帮助分析容器当前看到的根文件系统,却不能单独证明镜像来源,也不能替代 volume 取证。
docker commit 可以把容器当前状态保存成新镜像,但不应把它当成修复或证据保全的主要方式:
sudo docker commit --pause=false CONTAINER_ID incident-snapshot:case-123
它存在几个问题:
- 容器可写层可能包含攻击者修改;
- 挂载的 volume 不会因此自动变成镜像内容;
--pause=false会提高并发修改风险;默认暂停行为也会改变运行态;- 生成的镜像不是可信重建产物;
- 容器中的环境变量、密钥和临时文件可能被一并保存。
如果确实需要保存一个分析快照,应同时保存原容器 inspect、原镜像摘要、创建时间、命令执行者和快照镜像 ID,并将快照推送到隔离的取证仓库,而不是重新用于生产。
5. 对证据计算哈希,但理解哈希的意义
sudo sha256sum \
"$IR_ROOT/container-inspect.json" \
"$IR_ROOT/container-diff.txt" \
"$IR_ROOT/container-stdout-stderr.log" \
"$IR_ROOT/container-rootfs.tar" \
| sudo tee "$IR_ROOT/SHA256SUMS"
SHA-256 只能证明“之后拿到的文件与记录的字节相同”,不能证明文件内容本身真实,也不能证明采集时宿主机没有被攻击者修改。更完整的证据链还需要:
- 采集时间和时区;
- 采集命令;
- 执行人和主机标识;
- 原始文件权限;
- 证据存储的访问控制;
- 传输校验;
- 必要时使用签名或 WORM/不可变存储。
五、容器运行态取证:进程、环境和连接
1. 优先使用 daemon 提供的只读信息
sudo docker inspect --format \
'id={{.Id}}
name={{.Name}}
image={{.Image}}
created={{.Created}}
status={{.State.Status}}
pid={{.State.Pid}}
entrypoint={{json .Config.Entrypoint}}
cmd={{json .Config.Cmd}}' \
CONTAINER_ID
这里的 .Image 是容器创建时关联的镜像 ID。它比容器配置中的 Image 字符串更接近实际使用对象,但仍应和镜像本地元数据、仓库摘要以及发布记录交叉验证。
环境变量可能包含明文凭据:
sudo docker inspect --format '{{json .Config.Env}}' CONTAINER_ID \
> "$IR_ROOT/container-env.json"
该文件必须按秘密材料保护。环境变量泄露是事件响应中的二次风险:把它上传到普通工单、聊天系统或公开日志会扩大泄露范围。
2. docker exec 会改变证据
以下命令很常用:
sudo docker exec CONTAINER_ID ps aux
sudo docker exec CONTAINER_ID cat /proc/1/status
但它们会在容器内新建进程,可能触发恶意程序、改变进程树或被攻击者拦截。若容器已经暂停,docker exec 通常无法执行;这正是暂停保留运行态和执行交互式检查之间的冲突。
生产响应中应优先使用:
docker inspect;docker top;- 宿主机侧
/proc和 namespace 工具; - 宿主机审计日志;
- 受信任的取证工具。
如果必须 exec,应记录完整命令、时间和原因,并先确认这不会触发更严重的损害。
3. 宿主机侧检查容器进程
docker inspect 中的 .State.Pid 是容器 init 进程在宿主机 PID namespace 中的 PID。示例:
PID="$(sudo docker inspect --format '{{.State.Pid}}' CONTAINER_ID)"
sudo readlink "/proc/$PID/ns/mnt"
sudo readlink "/proc/$PID/ns/net"
sudo tr '\0' '\n' < "/proc/$PID/environ" \
> "$IR_ROOT/host-observed-container-environ.txt"
sudo cat "/proc/$PID/status" \
> "$IR_ROOT/host-observed-container-status.txt"
这类信息只能在宿主机仍然可信时使用。若攻击者已经获得宿主机 root,/proc 内容和 Docker 元数据都可能被伪造或删除。
还应采集网络连接,但命令取决于宿主机工具:
sudo nsenter -t "$PID" -n ss -tunap \
> "$IR_ROOT/container-network.txt" 2>&1 || true
nsenter 会进入目标进程的 namespace,ss 观察的是该网络 namespace 中的连接。它并不自动记录历史连接,因此只能说明采集时状态。历史外联需要依赖防火墙、流量镜像、代理、DNS 和云网络日志。
六、镜像追溯:从容器实例回到构建输入
1. 运行时实际使用的镜像
先查看容器关联的镜像 ID:
IMAGE_ID="$(sudo docker inspect --format '{{.Image}}' CONTAINER_ID)"
printf '%s\n' "$IMAGE_ID"
sudo docker image inspect "$IMAGE_ID" > "$IR_ROOT/image-inspect.json"
sudo docker image history --no-trunc "$IMAGE_ID" \
> "$IR_ROOT/image-history.txt"
如果容器最初由标签创建,例如:
docker run example/app:latest
则 latest 只是创建时的解析入口。之后仓库可以把 latest 指向另一个 manifest,不能仅凭当前标签判断旧容器运行的是哪个版本。应记录:
- 容器
.Image的本地 ID; - 镜像的
RepoDigests; - 镜像配置摘要;
- 创建时间;
- 入口命令;
- 镜像标签;
- 所有层的 digest;
- 容器创建时的 Compose、部署系统或 CI 记录。
查询仓库摘要:
sudo docker image inspect "$IMAGE_ID" \
--format '{{json .RepoTags}}
{{json .RepoDigests}}
{{.Id}}'
digest 是内容寻址标识。对于 OCI/Docker 镜像,manifest 或 index 的摘要通常形如 sha256:...。使用摘要可以把“想部署哪个对象”从可变标签转换为不可变内容引用:
services:
app:
image: registry.example.com/team/app@sha256:0123456789abcdef...
这解决的是引用漂移问题,但不自动证明镜像内容安全。一个被恶意构建出来的镜像同样可以拥有稳定 digest。
2. docker image history 能说明什么,不能说明什么
镜像历史可以展示构建层和部分命令:
sudo docker image history --no-trunc "$IMAGE_ID"
它可以帮助发现:
- 异常的下载命令;
- 不应存在的脚本;
- 以 root 执行的安装步骤;
- 可疑的基础镜像层。
但它不是完整的构建证明:
- BuildKit 可能合并、重排或优化层;
- shell 命令可能被脚本隐藏;
- 多阶段构建会丢弃中间阶段;
- 构建上下文、构建参数、秘密挂载和外部网络响应不一定完整出现在 history;
- history 中出现的命令不等于命令输出没有被篡改。
因此,追溯必须继续回到镜像仓库、CI/CD、Git 提交、构建日志、依赖锁文件和发布审批记录。
3. 区分四种“镜像来源”
事件响应时经常把以下概念混为一谈:
- 基础镜像来源:例如某个发行版或运行时镜像;
- 构建上下文:Dockerfile、源代码、依赖文件和复制进去的资源;
- 构建器:BuildKit、构建节点、构建权限和网络环境;
- 发布引用:仓库、tag、digest、签名和部署配置。
完整追溯链可以表示为:
Git commit
↓
Dockerfile + build context + lock files
↓
BuildKit builder + build parameters
↓
image manifest digest
↓
registry repository/tag/signature/SBOM
↓
Compose or deployment manifest
↓
container ID + runtime configuration
其中任一环节缺少记录,都可能只能得到“高度可能”,而不能得到确定结论。
4. SBOM、签名和扫描结果如何参与响应
SBOM 是软件物料清单,描述镜像中包含哪些软件包及其版本。它适合回答“哪些组件可能受某个漏洞影响”,但不等于运行时完整文件清单,也不证明镜像没有恶意代码。
Docker 环境中的 SBOM 和扫描能力依赖具体 Docker 版本、Docker Scout、仓库或第三方工具。执行前应确认插件和后端是否已安装,例如:
docker sbom "$IMAGE_ID"
如果命令不可用,不能因此推断“镜像没有 SBOM”;只说明当前客户端没有可用的 SBOM 能力。也可以从镜像仓库或 CI 产物获取 SBOM,并记录其生成对象的 digest。
签名用于证明某个身份签署了某个内容摘要。响应时应验证:
- 签名覆盖的是哪个 digest;
- 签名主体和信任根是否符合组织策略;
- 签名是在构建前、构建后还是发布后生成;
- 签名验证使用的策略是否与生产门禁一致。
签名验证通过只说明“该身份签署了该内容”,不说明签名者本身未被攻破,也不说明构建输入可信。
扫描结果具有时间和数据库版本。相同镜像在不同漏洞数据库时间点可能得到不同结果。应保存扫描工具版本、数据库时间、镜像 digest 和原始报告,而不是只保存一个“通过/失败”结论。
七、隔离后的调查:网络、对象和时间线
1. 不是只查一个容器
攻击者可能创建辅助对象,因此应枚举与事件时间窗口相关的:
sudo docker ps -a --no-trunc
sudo docker network ls
sudo docker volume ls
sudo docker images --digests --no-trunc
sudo docker ps -aq | xargs -r sudo docker inspect \
> "$IR_ROOT/all-containers-inspect.json"
还要检查:
- 是否有异常重启策略,例如
always或unless-stopped; - 是否有未知的 Docker secret、config 或 volume;
- 是否有 Compose 项目中的替代容器;
- 是否有定时任务、systemd unit、CI runner 或云编排系统在自动重建容器;
- 是否有其他容器挂载了同一个 volume;
- 是否有同一个镜像 digest 被部署到其他主机。
仅删除一个容器而不关闭自动编排,可能导致它立刻被重新创建,甚至让攻击者继续保留访问。
2. 建立时间线
至少应关联以下时间:
- 镜像构建时间;
- 镜像推送时间;
- 容器创建时间;
- 容器启动、停止和重启时间;
- Docker daemon 事件时间;
- 应用异常日志时间;
- Git 提交和合并时间;
- CI/CD 发布时间;
- 凭据创建、使用和轮换时间;
- 外部网络访问时间。
时间必须统一为 UTC,并保留原始时区信息。容器内的时间、宿主机时间、日志后端时间和云平台时间可能存在时钟偏差,不能机械地用字符串排序替代时间校准。
3. 用证据关联对象,而不是用名称关联对象
容器名称、镜像标签和 Compose 服务名都可能重复或变化。更可靠的关联键是:
- 容器完整 ID;
- 镜像 manifest digest;
- image config digest;
- volume 名称及宿主机数据标识;
- 网络 ID;
- Git commit;
- CI 构建 ID;
- 云资源 ID。
例如,两个都叫 web 的容器可能来自不同 Compose 项目;两个都叫 latest 的部署也可能使用不同 digest。
八、凭据和持久化数据:恢复前必须单独处理
1. 容器删除不会撤销凭据
如果凭据曾出现在以下位置:
- 环境变量;
- 镜像层;
- 容器可写层;
- 日志;
- shell history;
- 配置文件;
/var/run/docker.sock可访问路径;- 云实例元数据或挂载的凭据目录;
那么删除容器只会删除一个副本,不会让凭据自动失效。恢复前应轮换可能暴露的:
- 数据库密码;
- API token;
- 云访问密钥;
- SSH 密钥;
- TLS 私钥;
- registry 凭据;
- webhook secret;
- 应用 session signing key。
轮换顺序取决于依赖关系。对数据库密码,通常要先准备新凭据并验证新应用可以使用,再撤销旧凭据,避免恢复过程因凭据同时失效而中断。
2. volume 是独立的持久化对象
容器生命周期和 volume 生命周期不同:
sudo docker rm CONTAINER_ID
通常不会自动删除命名 volume。反过来,删除 volume 可能直接造成数据丢失。恢复前应先列出挂载关系,判断数据是否被多个服务共享。
对数据库 volume,不能只复制正在写入的目录作为一致备份。应优先使用:
- 数据库原生逻辑备份;
- 数据库支持的物理备份;
- 存储系统快照;
- 停机后复制;
- 文件系统一致性快照。
例如,直接在数据库仍运行时打包 data/ 目录,可能得到内部页、WAL、索引和元数据之间不一致的快照。这个归档也许能用于取证,但不应未经验证直接作为恢复源。
3. 先判断数据是否被篡改
恢复时要区分:
- 可恢复性:备份是否完整、可读取;
- 可信性:备份是否早于入侵、是否可能已经被攻击者修改;
- 一致性:数据库和相关文件是否处于一致状态;
- 合规性:恢复是否会重新引入敏感数据或违规配置。
一个“最近的备份”可能已经包含攻击者植入的账号、后门任务或被篡改的业务数据。因此,备份时间不能单独作为可信依据。
九、从可信输入重建,而不是把被入侵容器修补回来
1. 为什么不应把 docker commit 当作恢复方式
被入侵容器的当前状态包含未知修改。即使删除了恶意进程,容器可写层仍可能存在:
- 被替换的二进制文件;
- 修改过的启动脚本;
- 恶意 cron 或应用配置;
- SSH authorized keys;
- 后门用户;
- 下载的工具和凭据;
- 伪造的日志。
把该状态 commit 成新镜像,等于把未知状态固化并重新发布。事件响应可以保存它作为分析快照,但生产恢复应重新构建。
2. 使用可信基础镜像和锁定依赖
一个可追溯的构建例子:
# 生产中应将 digest 替换为组织批准并验证过的摘要
FROM registry.example.com/base/python:3.12@sha256:0123456789abcdef...
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --require-hashes -r requirements.txt
COPY src/ /app/src/
USER 65532:65532
ENTRYPOINT ["python", "-m", "src.main"]
这里有四个不同层面的控制:
- 基础镜像用 digest 固定内容;
- 依赖文件用 hash 固定包内容;
--no-cache-dir减少不必要的缓存材料;USER降低应用进程权限。
USER 65532:65532 只有在文件权限、监听端口和运行目录都允许非 root 用户工作时才成立。不能因为 Dockerfile 写了 USER 就假设应用实际没有能力写入敏感路径;仍需检查挂载、Capabilities 和宿主机权限映射。
3. Compose 恢复配置
services:
app:
image: registry.example.com/team/app@sha256:0123456789abcdef...
read_only: true
user: "65532:65532"
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp:rw,noexec,nosuid,nodev
environment:
APP_ENV: production
volumes:
- app-data:/var/lib/app
volumes:
app-data:
name: app-data-recovered
这些配置的含义是:
read_only: true使容器根文件系统只读,但不影响显式可写的 volume 和 tmpfs;cap_drop: ALL删除 Linux Capabilities,应用若需要特定能力,应逐项加回;no-new-privileges:true防止进程通过 setuid/setgid 或类似机制获得新的特权;tmpfs提供明确的临时写入位置;- 使用新 volume 名称可以避免无意中覆盖原始证据。
但 Compose 配置必须结合实际服务验证。只读根文件系统可能导致应用无法写 PID 文件、缓存、运行时 socket 或临时文件;此时应明确添加最小范围的 tmpfs 或 volume,而不是直接取消只读保护。
4. 恢复的状态转换
恢复不是“启动成功”这么简单,可以分为:
证据保全完成
→ 凭据已轮换
→ 镜像 digest 已验证
→ 配置经过审查
→ 备份已验证
→ 在隔离环境启动
→ 健康检查与安全控制验证
→ 恢复流量
→ 持续监控
在隔离环境启动时,应验证:
docker compose config
docker compose pull
docker compose up -d
docker compose ps
docker inspect "$(docker compose ps -q app)" \
--format 'status={{.State.Status}} user={{.Config.User}} readonly={{.HostConfig.ReadonlyRootfs}}'
docker compose config 用于展开并校验 Compose 配置;它不会证明镜像安全。docker compose pull 获取配置引用的镜像;如果引用使用 digest,应核对拉取后的 RepoDigests 或镜像 ID 与审批记录一致。
验证项目至少包括:
- 容器是否以预期 UID 运行;
- 根文件系统是否只读;
- 是否意外启用了
privileged、host namespace 或 Docker socket; - Capability 是否为预期集合;
- 对外端口是否最小;
- 健康检查是否通过;
- 应用是否只能访问必要的数据库和外部服务;
- 恢复后的数据是否经过完整性和业务一致性检查;
- 旧 token 是否已经失效。
十、常见失败路径和诊断方法
失败一:发现异常后直接 docker rm -f
表现是容器消失,后续只能看到“服务已重启”或“容器不存在”。这会丢失运行态、容器可写层关联和部分日志。
改进方式是先执行:
docker inspect
docker top
docker diff
docker logs --timestamps
docker inspect --format '{{json .Mounts}}'
只有在外传、破坏或宿主机风险迫切时,才优先强制停止,并记录为什么跳过常规保全。
失败二:只看 docker logs
如果攻击者没有使用标准输出,或者日志驱动已轮换,日志可能完全没有异常。必须同时检查容器配置、文件系统 diff、挂载、daemon 日志、宿主机审计和网络日志。
失败三:只看镜像 tag
app:latest 不能作为取证身份。应使用容器实际 image ID、manifest digest 和仓库审计记录。如果本地镜像已被清理,仍需从部署系统、镜像仓库和构建系统恢复 digest 关联。
失败四:导出容器后认为数据已经完整保存
docker export 不包含 volume。应分别保存:
- 容器根文件系统;
- 每个 named volume;
- 每个 bind mount 指向的宿主机路径;
- 相关数据库备份;
- 配置和 secret 的引用关系。
失败五:重建后漏洞扫描通过,就认为事件结束
扫描只能覆盖已知漏洞和工具能够识别的内容,不能证明:
- 凭据已经失效;
- 数据没有被篡改;
- Docker socket 没有被滥用;
- 宿主机没有被突破;
- CI/CD 和 registry 没有持续被控制;
- 其他相同镜像实例没有受到影响。
恢复完成后,还要搜索同一镜像 digest、同一凭据、同一外联域名、同一攻击者 IP 和同一异常文件在其他节点上的出现情况。
失败六:在不可信容器内执行大量调查命令
exec 产生新进程,也可能执行被替换的工具。调查命令越多,越可能改变现场。需要高可信证据时,应优先采用宿主机侧和外部日志系统,并对采集工具、传输链路和存储位置进行控制。
十一、一个可操作的最小响应剧本
下面的剧本适合风险尚未达到必须立即断电或隔离宿主机的场景。将 CONTAINER_ID 和 NETWORK_NAME 替换为已经确认的对象,不要直接复制到生产环境盲目执行。
set -u
export CASE="case-123"
export IR_ROOT="/var/ir/$CASE"
export CONTAINER_ID="replace-with-full-container-id"
sudo install -d -m 0700 "$IR_ROOT"
# 1. 建立初始快照
sudo docker version > "$IR_ROOT/docker-version.txt"
sudo docker inspect "$CONTAINER_ID" > "$IR_ROOT/container-before.json"
sudo docker top "$CONTAINER_ID" > "$IR_ROOT/top-before.txt" 2>&1 || true
sudo docker diff "$CONTAINER_ID" > "$IR_ROOT/diff-before.txt" 2>&1 || true
sudo docker logs --timestamps "$CONTAINER_ID" \
> "$IR_ROOT/logs-before.txt" 2>&1 || true
# 2. 保存镜像关联
IMAGE_ID="$(sudo docker inspect --format '{{.Image}}' "$CONTAINER_ID")"
printf '%s\n' "$IMAGE_ID" | sudo tee "$IR_ROOT/image-id.txt"
sudo docker image inspect "$IMAGE_ID" > "$IR_ROOT/image.json"
sudo docker image history --no-trunc "$IMAGE_ID" \
> "$IR_ROOT/image-history.txt" 2>&1 || true
# 3. 保存网络和挂载
sudo docker inspect --format '{{json .NetworkSettings.Networks}}' "$CONTAINER_ID" \
> "$IR_ROOT/networks.json"
sudo docker inspect --format '{{json .Mounts}}' "$CONTAINER_ID" \
> "$IR_ROOT/mounts.json"
# 4. 暂停,保留运行态
sudo docker pause "$CONTAINER_ID"
# 5. 重新读取状态,确认暂停成功
sudo docker inspect --format \
'status={{.State.Status}} paused={{.State.Paused}} pid={{.State.Pid}}' \
"$CONTAINER_ID" | sudo tee "$IR_ROOT/state-after-pause.txt"
接下来根据 networks.json 的结果断开非必要网络:
sudo docker network disconnect -f NETWORK_NAME "$CONTAINER_ID"
如果发现容器使用 host 网络、挂载 Docker socket、挂载宿主机根目录,或者存在宿主机级异常,应停止在容器层面操作,转入宿主机隔离和取证。若需要阻止继续执行,可执行:
sudo docker stop --time 10 "$CONTAINER_ID"
完成采集后,不要直接从原容器恢复生产。应:
- 轮换所有可能暴露的凭据;
- 从可信 Git 提交和批准的基础镜像重新构建;
- 固定镜像 digest;
- 在 Compose 或编排配置中重新应用非 root、Capabilities、Seccomp、只读根文件系统和网络限制;
- 使用经过验证的干净数据恢复;
- 在隔离环境验证;
- 最后恢复流量并持续监控。
十二、Linux 容器边界的最终判断
Docker 的容器 ID、镜像 digest、volume、网络 ID 和 daemon 事件共同组成了事件响应的对象模型:
container ID
├── image config / manifest digest
├── writable layer
├── mounts → volumes / host paths
├── network endpoints
├── process namespace and cgroups
└── logs and daemon events
其中任何一个分支都可能成为攻击路径或证据来源。删除容器只处理了对象本身,不能自动处理镜像仓库、volume、宿主机文件、网络流量、凭据和其他实例。
最可靠的恢复条件不是“容器又能启动”,而是:
这个表达式不是数学上的安全证明,而是恢复决策的必要条件集合:任何一项缺失,都可能让同一个事件通过镜像、数据、凭据或宿主机入口再次出现。对于 Linux 容器,最终还必须回答宿主机是否可信;如果答案是否定的,正确的恢复对象就不再是某个容器,而是经过重新安装、验证和密钥重建的宿主机或节点。
系列导航与关联阅读
- 系列入口:Docker 完整学习路线:从镜像与容器到安全、可观测和生产交付
- 上一篇:Docker 在 Linux、macOS 与 Windows 的差异:VM、路径、网络和性能
- 延伸:Docker 容器安全:非 root、Capabilities、Seccomp、只读根和隔离
- 延伸:Docker 软件供应链:SBOM、签名、扫描、可信基础镜像和策略门禁
官方资料
本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。

评论
0 条讨论