🐳 Docker究竟隔离了什么?— 从内核功能看容器的内部机制

容器不是虚拟机。然而,它们却感觉像是被隔离的。这是为什么呢?

##

##

🎯 本文涵盖内容

  • Docker(容器)实现隔离的两种Linux内核机制
  • Namespace — 隔离可见内容
  • cgroups — 限制可用资源
  • 隔离的内容 — 安全设计时必须了解的陷阱
  • 虚拟机(VM)与容器隔离级别的比较

📌 引言 / 背景

初学Docker时,最常听到的一句话是:

容器是隔离的环境。

没错。但是,它们隔离到什么程度呢?

实际上,许多开发人员和运维人员误以为容器是像VM一样完全独立的环境。结果,他们对容器逃逸(Container Escape)漏洞毫无防备,或者遇到单个容器独占整个宿主机CPU的情况。

要正确理解Docker的隔离,需要了解Linux内核层面发生了什么。今天,我们将深入探讨其内部机制。


🔍 1. Namespace — 隔离“可见内容”

Linux Namespace是内核功能,用于限制进程可以看到的系统资源范围。尽管在同一个宿主机上,容器内的进程感觉就像拥有自己的独立世界。

Docker总共使用了7种Namespace。

###

🗂️ PID Namespace — 进程ID隔离

在容器内执行`ps aux`时,只能看到自己的进程。宿主机上运行的数百个进程是不可见的。

容器内的第一个进程总是显示为PID 1。但在宿主机上查看时,它拥有完全不同的PID号。

# 在宿主机上查看
$ docker run --rm ubuntu sleep 1000 &
$ ps aux | grep sleep
root  12345  ...  sleep 1000   # 宿主机PID: 12345

# 在容器内查看
$ docker exec <container_id> ps aux
PID  USER  COMMAND
1    root  sleep 1000          # 在容器内是PID 1

###

🌐 Network Namespace — 网络隔离

每个容器都有自己的虚拟网络接口、IP地址和路由表。在容器A中执行`ifconfig`时,容器B的网络接口是不可见的。

Docker默认会创建一个名为`docker0`的桥接网络,并通过虚拟以太网对(veth pair)连接每个容器。

###

📁 Mount Namespace — 文件系统隔离

容器拥有自己的根文件系统(/)。这就是为什么运行Ubuntu镜像时`apt`能工作,运行Alpine镜像时`apk`能工作的原因。

宿主机的文件系统默认是不可见的,除非使用`-v`选项明确挂载。

###

👤 UTS Namespace — 主机名隔离

容器拥有自己的主机名(hostname)和域名。在容器内执行`hostname`时,显示的是容器ID,而不是宿主机的名称。

###

🔐 IPC Namespace — 进程间通信隔离

隔离System V IPC(共享内存、信号量、消息队列)。它阻止容器A中的进程访问容器B的共享内存。

###

👥 User Namespace — 用户ID隔离

容器内是`root`(UID 0)的用户,在宿主机上可以映射为普通用户。此功能是实现无根容器(Rootless containers)的关键,但在Docker的默认设置中并未启用。

###

🔗 Cgroup Namespace — cgroup视图隔离

限制容器只能看到自己的cgroup层次结构。(cgroup本身的说明将在下面立即介绍。)


🔍 2. cgroups — 限制“可用资源量”

cgroups (Control Groups)是内核功能,用于限制、测量和隔离进程组可以使用的系统资源量

如果说Namespace处理的是“可见内容”,那么cgroups处理的就是“可用资源量”。

Docker控制的主要资源如下:

资源 选项示例 说明
CPU –cpus=”1.5″ 最多使用1.5个核心
内存 –memory=”512m” 最大512MB RAM
磁盘I/O –blkio-weight=500 设置块I/O权重
网络 (tc联动) 带宽限制
# 运行一个CPU限制为1核、内存限制为512MB的容器
docker run --cpus="1.0" --memory="512m" nginx

# 查看实际的cgroup文件(在宿主机上)
cat /sys/fs/cgroup/memory/docker/<container_id>/memory.limit_in_bytes
# 536870912 (= 512MB)

如果没有cgroups会发生什么?一个恶意或有bug的容器可能会占用宿主机100%的CPU或耗尽所有内存,导致整个系统崩溃。这被称为“吵闹的邻居”问题(Noisy Neighbor problem)


⚠️ 未隔离的内容 — 安全漏洞由此产生

现在是最重要的部分。Docker容器无法隔离的内容。如果不知道这些,在云基础设施设计或安全审计中会犯致命错误。

❌ 内核本身是共享的

这是容器与VM最大的区别。

[VM]                          [컨테이너]
┌─────────────────────┐       ┌───────────────────────────┐
│  Guest OS Kernel A  │       │  Container A  Container B │
├─────────────────────┤       │  (Ubuntu)     (Alpine)    │
│  Guest OS Kernel B  │       ├───────────────────────────┤
├─────────────────────┤       │   ← 커널 공유 →           │
│   Hypervisor        │       │   Host Linux Kernel       │
├─────────────────────┤       ├───────────────────────────┤
│   Host OS Kernel    │       │   Host Hardware           │
└─────────────────────┘       └───────────────────────────┘

容器直接使用宿主机的Linux内核。因此,如果容器内的进程利用内核漏洞,整个宿主机都将面临风险。这就是容器逃逸(Container Escape)攻击的原理。

❌ 系统调用直接传递给宿主内核

容器内的进程调用`read()`、`write()`、`socket()`等系统调用时,这些请求会直接传递给宿主内核,没有中间层。不像VM那样有管理程序拦截和验证的层。

因此,Docker默认应用了Seccomp (Secure Computing Mode)配置文件来阻止危险的系统调用。但并非所有系统调用都能被阻止。

# 查看默认的Seccomp配置文件
docker inspect <container_id> | grep -i seccomp

###

❌ /proc, /sys 的部分内容是共享的

如果像`–pid=host`选项那样错误地与宿主机共享命名空间,通过`/proc`文件系统可能会暴露宿主机的进程信息。

###

❌ User Namespace 默认禁用

前面提到的User Namespace在Docker的默认设置中是禁用的。这意味着容器内的`root`在宿主机上也是`root`。如果与卷挂载结合,可能会导致对宿主机文件的`root`访问。

# 危险模式 — 将宿主机的根文件系统挂载到容器
docker run -v /:/host --rm -it ubuntu chroot /host
# 这样容器内就可以访问整个宿主机

##

💻 实践:直接查看 Namespace

# 查看容器的Namespace列表(在宿主机上)
CONTAINER_PID=$(docker inspect --format '{{.State.Pid}}' <container_id>)
ls -la /proc/$CONTAINER_PID/ns/

# 输出示例
lrwxrwxrwx 1 root root 0 ... cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 ... ipc    -> ipc:[4026532456]
lrwxrwxrwx 1 root root 0 ... mnt    -> mnt:[4026532454]
lrwxrwxrwx 1 root root 0 ... net    -> net:[4026532459]
lrwxrwxrwx 1 root root 0 ... pid    -> pid:[4026532457]
lrwxrwxrwx 1 root root 0 ... uts    -> uts:[4026532455]
# 如果user Namespace与宿主机相同?→ User Namespace 未使用状态
# 比较宿主机和容器的user namespace
ls -la /proc/1/ns/user        # 宿主机init进程
ls -la /proc/$CONTAINER_PID/ns/user  # 容器进程
# 如果符号链接的数字相同 → 相同的User Namespace = 危险

✅ 总结 / 结束语

分类 功能 隔离对象
Namespace PID, Network, Mount, UTS, IPC, User, Cgroup 资源的可见性
cgroups CPU, 内存, I/O, 网络 资源的使用量
未隔离 内核, 系统调用, User NS(默认) 安全边界的局限性

Docker的隔离功能强大但并非完美。核心总结如下:

  • 🟢 Namespace: 隔离进程、网络、文件系统等资源的可见性
  • 🟢 cgroups: 限制CPU、内存等资源的使用量
  • 🔴 内核共享: 容器使用相同的Linux内核,这是与VM的根本区别
  • 🔴 User Namespace默认禁用: 容器内的root = 宿主机的root

如果工作负载对安全性要求很高,请考虑以下几点:

  1. 无根容器(Rootless containers)启用User Namespace
  2. 应用Seccomp / AppArmor / SELinux配置文件
  3. 禁止使用`–privileged`选项
  4. 对于敏感工作负载,考虑使用gVisorKata Containers等增强型运行时

Comments

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注