mirror of
https://github.com/yeasy/docker_practice.git
synced 2026-11-09 19:57:37 +00:00
- 18.1 完整演示了如何开启 `userns-remap`,全程没有任何代价提示。但官方文档 明写「The containerd image store is not available when using user namespace remapping」,而本书基线是 Docker Engine v29.x——该版本新装默认就是 containerd image store,12.4 节还把它标为「强烈推荐(现代默认)」。照 18.1 做完,读者会在毫不知情的情况下退回旧存储后端,失去 OCI image index、多架构 镜像与构建溯源。补上这条互斥关系并交叉引用 12.4。 - 5 章说「生产环境建议指定确切版本号(如 nginx:1.28.0)而非仅大版本号」, 7 章说「使用主或次版本号而非完整版本号(patch)」并把 patch 版本列入 「避免」。两条都以全书建议的口吻写成,方向正好相反。它们其实面向不同环节: Dockerfile 的**基础镜像**用主/次版本号,才能在重新构建时自动带上安全补丁; **部署**一个服务镜像则应钉死确切版本,让部署可复现、升级是显式动作。 两处各补一句限定范围并互相指引,而不是删掉其中一条。
90 lines
5.3 KiB
Go
90 lines
5.3 KiB
Go
## 18.1 内核命名空间
|
||
|
||
命名空间 (Namespace) 是 Linux 容器隔离的基础,它确保了容器内的进程无法直接干扰主机或其他容器。虽然在本书第 12 章中我们已经从底层实现的角度介绍了 Namespace,但在本节中,我们将重点探讨其 **安全意义** 及相关配置。
|
||
|
||
### 18.1.1 隔离的安全本质
|
||
|
||
Docker 守护进程在启动容器时,会在后台为容器创建一套独立的命名空间。命名空间提供了最基础也是最直接的隔离:
|
||
|
||
- **PID Namespace**:防止容器内的进程查看或终止宿主机或其他容器的进程。恶意攻击者即使在容器内获得了 root 权限,也无法通过 `kill` 命令影响宿主机上的关键服务。
|
||
- **NET Namespace**:每个容器都有自己独立的网络栈。如果没有显式地进行端口映射或将容器连接到同一网络,容器之间无法网络互通,从而限制了横向移动的能力。
|
||
- **MNT Namespace**:为容器提供独立的文件系统视图。这可以防止容器不经意或恶意地修改宿主机的重要系统文件(如 `/etc/passwd`)。
|
||
|
||
### 18.1.2 命名空间不是绝对安全的护城河
|
||
|
||
尽管命名空间提供了很好的隔离性,但我们必须认识到:**所有的容器依然共享同一个宿主机的 Linux 内核**。
|
||
|
||
这意味着,一旦宿主机的内核存在提权漏洞(如著名的 Dirty COW 漏洞),攻击者有可能通过突破 Namespace 的限制,直接在内核层面执行恶意代码,从而实现“容器逃逸”。
|
||
|
||
> [!WARNING]
|
||
> 为了缓解内核漏洞带来的威胁,生产环境务必保持宿主机 Linux 内核的及时修补与更新,或者借助诸如 gVisor、Kata Containers 等提供了独立内核的安全容器技术。同时,需要及时修补容器运行时(如 runC)的漏洞。2025 年 11 月披露的一系列 runC 容器逃逸漏洞(CVE-2025-31133、CVE-2025-52565、CVE-2025-52881)就表明,即使内核保持更新,运行时层的缺陷仍然可能导致容器隔离被突破。
|
||
|
||
通过命名空间,Docker 也能限制进程从外部环境获取信息。
|
||
例如,由于进程环境被隔离,进程在内部通常无法直接感知外部宿主机的进程和挂载命名空间。网络命名空间隔离的是网络栈;默认 bridge 网络通常允许容器主动访问外部网络,限制的是外部直接访问容器服务。若要限制出站访问,需要使用 `none` 网络、防火墙、Kubernetes NetworkPolicy 或运行时策略。
|
||
|
||
### 18.1.3 用户命名空间与提权防护
|
||
|
||
在所有的 Namespace 中,**User Namespace** 对安全的影响尤为关键。
|
||
|
||
在默认情况下,容器内的 `root` 用户(UID=0)就是宿主机上的 `root` 用户。如果攻击者设法突破了容器的其他隔离机制获取了宿主机的访问权限,他将拥有宿主机的最高系统权限。
|
||
|
||
通过启用 **User Namespace Remapping(用户命名空间映射)**,我们可以将容器内的 `root` 用户映射到宿主机上的一个无特权普通用户。
|
||
|
||
#### 如何配置 User Namespace
|
||
|
||
要在 Docker 服务端启用这一特性,需要修改 Docker 的配置文件 `/etc/docker/daemon.json`。
|
||
|
||
1. **设置映射策略**
|
||
|
||
编辑配置文件,添加 `userns-remap` 配置项:
|
||
|
||
```json
|
||
{
|
||
"userns-remap": "default"
|
||
}
|
||
```
|
||
使用 `default` 值时,Docker 会自动在宿主机上创建一个名为 `dockremap` 的用户和用户组。
|
||
|
||
> **注意其代价**:官方文档明确 **containerd image store 在启用 `userns-remap` 时不可用**。
|
||
> 而本书基线是 Docker Engine v29.x,该版本新装默认就是 containerd image store,
|
||
> [12.4 节](../12_implementation/12.4_ufs.md) 也把它标为「强烈推荐(现代默认)」。
|
||
> 也就是说,打开 userns-remap 会让你退回旧的镜像存储后端,失去 OCI image index、
|
||
> 多架构镜像与构建溯源等能力。两者只能二选一,请按威胁模型权衡。
|
||
|
||
2. **验证子 UID 和子 GID 分配**
|
||
|
||
Docker 会通过 `/etc/subuid` 和 `/etc/subgid` 文件为 `dockremap` 分配一个高位的 UID 范围:
|
||
|
||
```bash
|
||
$ cat /etc/subuid
|
||
dockremap:165536:65536
|
||
```
|
||
这意味着:容器内的 UID `0`(root 用户)在宿主机上实际被映射成了 UID `165536`。如果是容器内的 UID `1`,对应宿主机的 `165537`,以此类推。
|
||
|
||
3. **重启 Docker 守护进程**
|
||
|
||
```bash
|
||
$ sudo systemctl restart docker
|
||
```
|
||
|
||
#### 验证映射效果
|
||
|
||
我们可以运行一个简单的容器并执行 `sleep` 命令,同时在宿主机上观察进程的所有者:
|
||
|
||
```bash
|
||
## 在容器内以 root 身份运行
|
||
$ docker run -d --name userns_test alpine sleep 3600
|
||
|
||
## 在宿主机上查看该 sleep 进程
|
||
$ ps aux | grep sleep
|
||
165536 12345 0.0 0.0 1568 4 ? Ss 14:20 0:00 sleep 3600
|
||
```
|
||
你会发现,尽管在容器内该进程是由 root 启动的,但在宿主机上,它的属主是 `165536`(一个完全没有特权的用户)。
|
||
|
||
> [!TIP]
|
||
> 启用 User Namespace 会对容器共享宿主机数据卷(Bind Mount)产生权限影响。你需要确保映射后的高位 UID 对宿主机上的挂载目录具有合适的读写权限。
|
||
|
||
### 18.1.4 总结
|
||
|
||
内核命名空间从 Linux 2.6.15 版本(2006 年)被引入,十余年间,这些机制的可靠性在诸多大型生产系统中被实践验证。通过合理利用命名空间(尤其是 User Namespace),可以极大地收窄攻击面,显著提升容器部署的安全性。
|