这一篇要彻底搞懂:容器到底靠什么隔离?为什么它不像虚拟机那么安全?
一、容器是什么
容器是操作系统级别的虚拟化 :它不虚拟出一整套硬件,而是让多个进程共用同一个 Linux 内核,只是给每个进程组套上"隔离罩"。
1.1 容器 vs 虚拟机
| 对比项 | 虚拟机(VM) | 容器 |
|---|---|---|
| 隔离层次 | 硬件级,各自有独立内核 | 进程级,共享宿主内核 |
| 启动速度 | 分钟级 | 毫秒~秒级 |
| 体积 | GB 级 | MB 级 |
| 隔离强度 | 强(Hypervisor 隔离) | 较弱(内核出问题就一起完蛋) |
| 逃逸难度 | 需攻破 Hypervisor | 内核/配置有缺陷即可逃逸 |
核心安全结论 :容器共享内核,所以内核漏洞、危险配置都能导致逃逸。这也是第 03 篇存在的原因。
二、容器隔离的三大支柱
容器的"隔离罩"由三样东西组成,缺一不可:
2.1 Namespace(命名空间)------ 负责"看不见"
Namespace 让容器里的进程看不到宿主机的其他资源。Linux 有 8 种:
| Namespace | 隔离内容 | 看不到什么 |
|---|---|---|
mnt (Mount) |
挂载点/文件系统 | 宿主机的目录树 |
pid (PID) |
进程号 | 宿主机的其他进程 |
net (Network) |
网卡、IP、端口、路由 | 宿主机的网络 |
ipc (IPC) |
进程间通信(信号量、共享内存) | 宿主机的 IPC |
uts (UTS) |
主机名和域名 | 宿主机 hostname |
user (User) |
用户和用户组 ID | 宿主机的用户(容器内 root 可映射成宿主普通用户) |
cgroup |
cgroup 视图 | 宿主机的 cgroup 树 |
time |
系统时钟(较新) | 宿主机的时钟偏移 |
实验:在容器里查看自己的 namespace
docker run --rm alpine:3.19 ls -l /proc/self/ns
逐段解释:
-
docker run:启动一个容器。 -
--rm:容器退出后自动删除,避免堆积垃圾容器。 -
alpine:3.19:使用的镜像,Alpine 是一个只有几 MB 的极小 Linux。 -
ls -l /proc/self/ns:/proc/self是当前进程自己的信息目录,ns子目录列出该进程所属的各种 namespace。
关键理解 :容器里每个 namespace 后面都带一个 [...] 编号(如 mnt:[4026532...])。如果和宿主机上 ls -l /proc/1/ns 的编号不同,就说明被隔离了。
2.2 Cgroups(控制组)------ 负责"用多少"
Namespace 管"看得见看不见",Cgroup 管"能用多少":CPU、内存、磁盘 IO、进程数上限。
为什么和安全有关 :cgroup 的某些功能(尤其 cgroup v1 的 release_agent)历史上被用作逃逸入口,第 03 篇会讲。
实验:确认你的环境是 cgroup v2
stat -fc %T /sys/fs/cgroup
-
stat:查看文件/文件系统信息。 -
-f:显示文件系统信息。 -
-c %T:只输出文件系统类型。 -
输出
cgroup2fs表示 cgroup v2;输出tmpfs表示 cgroup v1。
可能踩坑 :本实验环境是 cgroup v2 (输出
cgroup2fs)。网上很多老教程的release_agent逃逸手法在 cgroup v2 上无效。
2.3 联合文件系统(UnionFS / overlayfs)------ 负责"文件从哪来"
容器镜像是一层层叠加的只读层,最上面加一个可写层。容器运行时的写操作都落在可写层,删掉容器可写层就没了。
-
好处:镜像分层复用,省空间、启动快。
-
安全含义:镜像层是只读 的,但可写层和挂载点是攻击者重点利用的地方(如挂载宿主机目录)。
实验:查看存储驱动
docker info | grep -i "storage driver"
输出 Storage Driver: overlayfs(旧版本叫 overlay2)。
三、Docker 架构与组件
理解组件关系,才能理解逃逸的"跳板"在哪里:
docker 命令行
│ (REST API,默认走 /var/run/docker.sock)
▼
dockerd(Docker 守护进程,接收 API 请求)
│
▼
containerd(容器生命周期管理)
│
▼
runc(真正调用内核 namespace/cgroup 创建容器的工具)
│
▼
容器进程
| 组件 | 作用 | 安全关注点 |
|---|---|---|
docker CLI |
客户端 | - |
dockerd |
守护进程,监听 socket | docker.sock 泄露 = 控制整台宿主机 |
containerd |
运行时管理 | containerd.sock 同理 |
runc |
底层创建容器 | runc 漏洞 = 容器逃逸(CVE-2019-5736) |
| OCI | 容器标准规范 | 镜像/运行时遵循标准 |
实验:查看本机 Docker 组件
docker info | grep -Ei "server version|cgroup|runtime|storage"
你会看到类似:
Server Version: 29.1.3
Storage Driver: overlayfs
Cgroup Driver: systemd
Cgroup Version: 2
Runtimes: io.containerd.runc.v2 runc
Default Runtime: runc
逐行理解:
-
Server Version:dockerd 版本。 -
Storage Driver:文件系统驱动。 -
Cgroup Driver: systemd:cgroup 由 systemd 管理。 -
Cgroup Version: 2:内核 cgroup 版本。 -
Runtimes ... runc:底层运行时是 runc。
四、镜像与 Dockerfile 安全
4.1 镜像分层
docker history alpine:3.19
逐层看镜像怎么构建的。安全含义:
-
镜像层里删掉的文件仍在历史层 里,可能泄露密钥(例如构建时
COPY了.env再RUN rm,历史层里还在)。 -
私有镜像仓库未授权可拉取,等于源码/凭证泄露。
4.2 Dockerfile 常见安全问题
| 问题 | 例子 | 风险 |
|---|---|---|
| 硬编码密钥 | ENV AK=xxx |
镜像里可提取 |
| 使用 root 运行 | 默认就是 root | 逃逸后直接是宿主 root |
| 基础镜像过旧 | FROM ubuntu:16.04 |
含大量已知漏洞 |
| 安装不必要的包 | apt install curl vim gcc |
攻击者拿到后工具齐全 |
ADD 远程文件 |
ADD http://... / |
不可控内容 |
| 未固定版本 | FROM node:latest |
供应链不确定性 |
最佳实践:
-
用
USER nonroot以非 root 运行。 -
多阶段构建,最终镜像不含构建工具和密钥。
-
用
hadolint、trivy扫描镜像漏洞和密钥。
五、危险配置项(逃逸的"温床")
这是本篇最重要的部分。下面每一条都是第 03 篇的伏笔。
| 配置 | 含义 | 危害等级 |
|---|---|---|
--privileged |
给容器几乎全部能力 + 访问所有设备 | ★★★★★ |
-v /var/run/docker.sock:/var/run/docker.sock |
把 Docker 控制权给容器 | ★★★★★ |
-v /:/host |
挂载宿主机根目录 | ★★★★★ |
--pid=host |
共享宿主机 PID 命名空间 | ★★★★ |
--network=host |
共享宿主机网络 | ★★★ |
--cap-add=SYS_ADMIN |
增加系统管理能力 | ★★★★ |
--cap-add=SYS_PTRACE |
可调试/注入其他进程 | ★★★ |
--user=0(默认) |
容器内是 root | ★★★ |
| 未启用 seccomp/AppArmor | 缺少系统调用过滤 | ★★ |
5.1 Capabilities(能力)是什么
Linux 把 root 的超级权限拆成了几十种"能力"(capability),容器默认丢弃 了大部分危险能力。--privileged 相当于把能力全加回来。
实验:查看容器默认能力 vs 特权容器能力
docker run --rm alpine:3.19 grep Cap /proc/self/status
docker run --rm --privileged alpine:3.19 grep Cap /proc/self/status
逐段解释:
-
grep Cap /proc/self/status:从进程状态文件里过滤能力字段。 -
输出里
CapEff表示当前生效的能力位图(十六进制)。 -
对比两条命令的
CapEff,特权容器的值明显更大(本环境特权容器是000001ffffffffff,即全部能力)。
可能踩坑 :
CapEff是十六进制位图,人眼看不出含义。可以用capsh --decode=000001ffffffffff解码(需安装libcap2-bin)。
5.2 seccomp 与 AppArmor
-
seccomp :过滤容器能调用的系统调用。Docker 默认启用一个白名单,阻止如
mount、reboot等危险调用。 -
AppArmor / SELinux:强制访问控制,进一步限制容器能碰的文件。
--privileged 会关闭这些限制。
六、动手实验:感受隔离边界
以下命令都在远端执行。
1)容器共享宿主机内核
docker run --rm alpine:3.19 uname -a
回显里的内核版本与宿主机 uname -a 完全一致,这就是"共享内核"的铁证。
2)容器默认看不到宿主机进程
docker run --rm alpine:3.19 ps -ef
通常只看到容器内自己的几个进程(因为 PID namespace 隔离)。对比在宿主机执行 ps -ef,进程数量完全不同。
3)容器默认有独立网络
docker run --rm alpine:3.19 ip addr
会看到容器自己的 eth0,IP 一般是 172.17.0.x(Docker 默认网桥网段)。宿主机上则有一个 docker0 网桥(本环境是 172.17.0.1)。
4)容器内是 root,但通常逃不出去
docker run --rm alpine:3.19 id
输出 uid=0(root)------注意,这个 root 只是容器 namespace 内的 root ,默认情况下它没有权限动宿主机的核心资源。只有配置了特权/挂载等,才会真正危险。
七、防御要点(本篇版)
-
永远不要用
--privileged,除非你完全清楚在做什么。 -
不要把
docker.sock挂进容器;必须用就通过受限代理(如 docker-socket-proxy)只开放必要 API。 -
容器以非 root 用户 运行:
docker run --user 1000:1000 ...。 -
按需授予能力,避免
--cap-add=SYS_ADMIN:--cap-drop=ALL --cap-add=NET_BIND_SERVICE。 -
不要
--pid=host / --network=host,除非确有需要。 -
启用 seccomp、AppArmor(Docker 默认已开,别关)。
-
用
trivy/grype扫描镜像漏洞,用hadolint检查 Dockerfile。 -
宿主机及时打内核补丁。
八、本篇小结
-
容器 = 共享内核 + namespace 隔离 + cgroup 限额 + 联合文件系统。
-
namespace 管"看不见",cgroup 管"用多少",UnionFS 管"文件从哪来"。
-
Docker 调用链:
docker → dockerd → containerd → runc → 容器。 -
危险配置是逃逸的根本原因:
--privileged、docker.sock、-v /:/host、--pid=host。 -
本环境是 cgroup v2,部分老逃逸手法失效。