Docker 资源隔离机制:Namespaces、Cgroups、UnionFS 与安全加固
Docker 的资源隔离并非 Docker 自身发明,而是对 Linux 内核已有特性 的封装与组合使用。核心依赖三大机制:Namespaces(命名空间)、Cgroups(控制组) 和 UnionFS(联合文件系统),辅以 Seccomp、Capability、AppArmor 等安全加固手段。
Namespaces — 视图隔离(“你能看到什么”)
Namespace 让进程以为自己独占整个系统。Docker 使用了 6 种 namespace:
| Namespace | 隔离内容 | 系统调用 |
|---|---|---|
| PID | 进程 ID | CLONE_NEWPID |
| NET | 网络栈(接口、端口、路由表) | CLONE_NEWNET |
| IPC | 进程间通信(信号量、消息队列、共享内存) | CLONE_NEWIPC |
| MNT | 文件系统挂载点 | CLONE_NEWNS |
| UTS | 主机名和域名 | CLONE_NEWUTS |
| USER | 用户和用户组 UID/GID 映射 | CLONE_NEWUSER |
例子:容器内 PID 1 的进程,在宿主机上实际可能是 PID 24567。容器内的 ps 只能看到自己 namespace 里的进程。
底层 API:unshare() 系统调用创建新 namespace,setns() 加入已有 namespace。
Cgroups — 资源限制(“你能用多少”)
Cgroups(Control Groups)限制、记录和隔离进程组使用的物理资源:
| 子系统 | 限制内容 |
|---|---|
| cpu | CPU 时间份额(cpu.shares)、核心数(cpu.cfs_quota_us) |
| memory | 内存使用上限(memory.limit_in_bytes)、swap 限制 |
| blkio | 块设备 I/O 带宽 |
| devices | 可访问的设备列表 |
| pids | 最大进程数(pids.max) |
| net_cls | 网络包标记(配合 tc 做带宽限制) |
Docker 对应参数:
docker run --memory=512m --cpus=1.5 --pids-limit=100 nginx对应在宿主机上的路径(cgroup v2):
/sys/fs/cgroup/docker/<container-id>/memory.max/sys/fs/cgroup/docker/<container-id>/cpu.maxUnionFS — 文件系统隔离
通过分层镜像(layered image)实现存储隔离:
┌─────────────────────────┐│ 可写层 (Container Layer) │ ← 容器运行时的修改写这里├─────────────────────────┤│ Image Layer 3 (nginx) │├─────────────────────────┤│ Image Layer 2 (deps) │ ← 只读层,多个容器共享├─────────────────────────┤│ Image Layer 1 (base OS)│└─────────────────────────┘- Copy-on-Write (CoW):修改文件时才从下层复制到可写层,节省磁盘和内存
- Docker 支持多种 UnionFS 后端:overlay2(默认)、btrfs、zfs 等
安全加固(额外隔离层)
| 机制 | 作用 |
|---|---|
| Seccomp | 限制容器可用的系统调用(白名单,约 300 个 syscalls 中默认允许约 50 个) |
| AppArmor / SELinux | 强制访问控制(MAC),限制文件访问权限 |
| Capability | 丢弃不必要的 Linux capabilities(如 CAP_NET_ADMIN、CAP_SYS_ADMIN) |
| User Namespace | 容器内 root → 宿主机普通用户的 UID 映射 |
| netfilter/iptables | 容器间网络隔离(bridge 网络 + 规则) |
整体架构
┌───────────────────────────────────────────┐│ Container Process ││ ┌─────────┐ ┌───────┐ ┌───────────────┐ ││ │ PID ns │ │ NET ns│ │ USER ns │ ││ │ "我看到" │ │"我的IP"│ │ "我是 root" │ ││ └─────────┘ └───────┘ └───────────────┘ │├───────────────────────────────────────────┤│ Cgroup 限制 ││ CPU: 1.5 核 | 内存: 512MB | PIDs: 100 │├───────────────────────────────────────────┤│ UnionFS (overlay2) ││ 只读镜像层 + 可写容器层 (CoW) │├───────────────────────────────────────────┤│ Linux Kernel ││ Seccomp + AppArmor + Capabilities │└───────────────────────────────────────────┘一句话总结
- Namespace:让进程”看”不到别人(隔离视图)
- Cgroup:让进程”用”不了太多(限制资源)
- UnionFS:让进程”改”不到镜像(分层存储)
- Seccomp/Capability/MAC:让进程”干”不了坏事(安全加固)
Docker 本质上就是对这些 Linux 内核特性的封装和组合使用,它本身并不包含这些隔离机制——这些是内核提供的,Docker 只是通过 runc(OCI runtime)调用了相应的 API。
深入 Seccomp(Secure Computing Mode)
核心原理
Linux 有大约 300-400 个系统调用(syscalls),比如 open, read, write, execve, mount, reboot 等。大多数容器在正常运行时只需要其中一小部分。
Seccomp 做的事情就是:白名单/黑名单过滤系统调用,非法调用直接返回错误或杀掉进程。
工作流程:
容器进程调用 syscall (如 mount) │ ▼ ┌──────────────┐ │ Seccomp 过滤器 │ ← BPF 规则匹配 └──────┬───────┘ │ ┌────┴────┐ │ │ 允许 拒绝 │ │ ▼ ┌───┴────┐ 正常 │ │ 执行 ENOSYS SIGSYS (继续) (返回 (杀掉 错误) 进程)两种模式
| 模式 | 说明 |
|---|---|
| strict(经典模式) | 只允许 read, write, _exit, sigreturn 四个调用,其他一律 SIGSYS 杀进程。太严格,基本没人用。 |
| filter(BPF 模式) | 用 BPF 规则自定义允许/拒绝哪些调用。Docker 用的就是这个。 |
BPF(Berkeley Packet Filter)原本是做网络包过滤的,内核把它复用为通用的规则引擎——seccomp-bpf 就是用 BPF 程序来决定每个 syscall 的命运。
Seccomp 的动作(Actions)
当 syscall 命中规则时,可以执行以下动作:
| Action | 效果 |
|---|---|
SECCOMP_RET_ALLOW | 放行,正常执行 |
SECCOMP_RET_ERRNO | 返回错误码(通常是 ENOSYS:系统调用未实现),进程不死 |
SECCOMP_RET_KILL_THREAD | 杀掉当前线程 |
SECCOMP_RET_KILL_PROCESS | 杀掉整个进程 |
SECCOMP_RET_TRACE | 交给 ptrace 处理(调试场景) |
SECCOMP_RET_LOG | 放行但记录日志 |
SECCOMP_RET_TRAP | 发送 SIGSYS 信号(可被捕获处理) |
Docker 默认对不允许的 syscall 使用 SECCOMP_RET_ERRNO(返回错误,不杀进程),这样容器内程序会收到”此调用不可用”的反馈,而不是直接崩溃。
Docker 的默认 Seccomp Profile
Docker 有一个内置的默认 profile(约 300 行 JSON),定义了默认的过滤规则:
{ "defaultAction": "SCMP_ACT_ERRNO", "defaultErrnoRet": 1, "architectures": ["SCMP_ARCH_X86_64"], "syscalls": [ { "names": ["read", "write", "open", "close", "..."], "action": "SCMP_ACT_ALLOW" }, { "names": ["mount", "umount2"], "action": "SCMP_ACT_ERRNO" } ]}逻辑:
defaultAction: ERRNO→ 默认拒绝所有 syscall- 然后白名单列出允许的约 50-60 个关键 syscall
- 一些调用有条件允许(带参数过滤)
默认允许的典型 syscall
read, write, open, close, stat, fstat, poll, mmap, mprotect,munmap, brk, ioctl, access, pipe, select, mremap, mmap,clone, fork, vfork, execve, exit, wait4, getpid, getuid,getgid, getppid, dup, dup2, socket, connect, accept,bind, listen, recvmsg, sendmsg, ...默认禁止的典型 syscall
mount, umount2, reboot, kexec_load, open_by_handle_at,init_module, delete_module, iopl, ioperm, swapon, swapoff,syslog, perf_event_open, fanotify_init, kcmp,add_key, request_key, keyctl, ...这些被禁止的调用基本都是内核管理类操作——容器不应该挂载磁盘、加载内核模块、重启系统。
自定义 Seccomp Profile
你可以写自己的 JSON profile:
{ "defaultAction": "SCMP_ACT_ERRNO", "syscalls": [ { "names": ["chmod", "chown"], "action": "SCMP_ACT_ERRNO" }, { "names": ["read", "write", "open", "close"], "action": "SCMP_ACT_ALLOW" } ]}使用自定义 profile:
docker run --security-opt seccomp=/path/to/profile.json nginx或者完全禁用 seccomp(不推荐,仅调试用):
docker run --security-opt seccomp=unconfined nginx参数级过滤(高级)
Seccomp-bpf 不仅可以根据 syscall 名字过滤,还能根据参数值过滤:
{ "names": ["socket"], "action": "SCMP_ACT_ERRNO", "args": [ { "index": 0, "op": "SCMP_CMP_NE", "value": 2 } ], "comment": "只允许 AF_INET (2),其他 socket domain 拒绝"}args 字段:
index:参数位置(从 0 开始)op:比较运算符(EQ,NE,LT,LE,GT,GE等)value:比较值
这样可以实现非常精细的控制,比如”允许 clone 但不允许 CLONE_NEWUSER flag”。
Seccomp 在 Docker 中的加载位置
容器进程 │ ▼syscall (如 mount) │ ▼┌────────────────────────────────┐│ Seccomp BPF 过滤器 │ ← runc 在容器启动时加载│ (从 JSON profile 编译而来) │└────────────────────────────────┘ │ │ ALLOW ERRNO │ │ ▼ ▼执行 mount 返回 ENOSYS "Function not implemented"加载时机:runc(Docker 的容器运行时)在 clone 创建容器进程后、执行用户程序前,通过 prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) 加载 BPF 程序。一旦加载,不可撤销(单向门)。
与其他安全机制的关系
| 机制 | 过滤粒度 | 特点 |
|---|---|---|
| Seccomp | 系统调用级别 | 限制”能调什么” |
| Capability | 权限级别 | 限制”有什么特权”(如 CAP_SYS_ADMIN) |
| AppArmor/SELinux | 文件/资源级别 | 限制”能访问什么文件/做什么操作” |
三者互补:
- Seccomp 说:你不能调
mount - Capability 说:就算你能调
mount,你没有CAP_SYS_ADMIN也白搭 - AppArmor 说:就算你有
CAP_SYS_ADMIN,这个路径你也不许写
实际案例:CVE-2019-5736
CVE-2019-5736(runc 容器逃逸漏洞):攻击者可以通过 /proc/self/exe 覆盖宿主机上的 runc 二进制文件。Seccomp 的默认 profile 禁止了对 /proc/self/exe 的 write 相关操作(通过禁止 ptrace 等相关调用),在部分场景下阻断了此攻击路径。
没有 seccomp 时,容器内的进程理论上可以调用全部 300+ 个 syscall,攻击面巨大。有了 seccomp,攻击面缩减到约 50 个,大幅降低了内核漏洞被利用的风险。