一篇文章搞懂Linux 文件系统隔离:Mount Namespace 与三个挂载视图 容器安全3/7
容器安全文章:3
核心概念:Mount Namespace(挂载命名空间)与文件系统视图隔离
Linux 文件系统隔离:Mount Namespace 与三个挂载视图
为什么需要隔离?
一台服务器跑着网站、数据库、缓存三个服务。如果共享同一个文件系统,一个服务被入侵就能读到其他服务的配置文件、数据库密码。文件系统隔离让每个服务只能看到自己的文件,即使运行在同一台机器上。
下面是实验证明:
- Linux 系统:debian
- 实验能更好了解和证明
一、核心概念
1.1 目录是门,文件系统是房间
目录本身不存储数据,它只是一扇门。门后面是什么房间,取决于你在这扇门上"挂载"了什么。
文件系统是独立的房间——有自己的文件、目录、权限。但不挂到门上,你就进不去、看不到、用不了。
# 造一扇门(创建一个空目录)mkdir/mnt/door# 门后面是空的ls/mnt/door# => (没有任何输出)# 在门后挂一个房间# mount = 挂载命令# -t tmpfs = 指定房间类型为 tmpfs(内存文件系统,存在内存里,重启消失)# 第一个 tmpfs = 房间类型# 第二个 tmpfs = 房间名字(可以随便取)# /mnt/door = 门的位置mount-ttmpfs tmpfs /mnt/door# 现在门后就是一个独立房间了echo"我在房间里">/mnt/door/msg.txtcat/mnt/door/msg.txt# => 我在房间里# 拆下房间,门又变空了:# umount = 卸载,把房间从门上拆下来umount/mnt/doorcat/mnt/door/msg.txt# => 报错:没有那个文件或目录📌文件系统必须挂载到目录上才能使用。目录是唯一的入口。
1.2 什么是 Mount Namespace(挂载视图)
Linux 内核有一个机制叫Mount Namespace(挂载命名空间),本文简称"挂载视图"。
它的作用:
让每个进程拥有自己的"房间清单"(挂载表),进程之间互相独立。
同一个门/mnt/data,进程 A 的清单上写着通向房间 X,进程 B 的清单上可能写着通向房间 Y,或者什么都没写。
不是房间变了,是进程手里的清单不同。
1.3 什么是"复制一份"
创建新挂载视图时,内核会复制一份当前的房间清单给新视图。之后两边的清单各自更新,互不影响。
就像你复印一份文件清单,你在这份复印件上新增一条记录,原件不会自动更新
⚠️重要:挂载视图隔离的是"房间清单"(挂载表),不是房间里的文件内容。
如果两个视图都挂载了同一个房间(比如都指向同一个硬盘分区),修改文件会互相看到。只有新挂载的房间才是隔离的。
二、第一视图:宿主机的清单
这是你开机后的正常 Linux。所有进程共享同一份房间清单。
你在第一视图 终端:ls/=>bin boot dev etc home lib...特点:
- 所有挂载操作全系统可见
- 其他视图的新挂载,你看不到
- 这是"根视图",其他视图都从这里复制出去
三、第二视图:独立的清单
3.1 怎么创建?
sudo=以 root 权限执行 unshare=脱离共享资源,创建自己的副本--mount=只脱离"房间清单"这一项--propagationprivate=确保新视图的挂载操作不影响宿主机 (systemd 默认把根文件系统的挂载传播类型设为 shared,不加这句可能传播回宿主机) /bin/bash=启动一个新的命令行 终端:sudounshare--mount--propagationprivate /bin/bash执行后,你的 shell 进入了一个新的挂载视图。这一刻,内核复制了一份房间清单给你。从此你的清单和宿主机的清单各自独立。
3.2 验证视图切换
查看当前进程的 Mount Namespace ID 终端: readlink /proc/$$/ns/mount=>mnt:[4026532234](数字与之前不同,说明进了新视图)3.3 即时验证
# 你在第二视图mkdir/mnt/room2mount-ttmpfs tmpfs /mnt/room2echo"第二视图的文件">/mnt/room2/secret.txtcat/mnt/room2/secret.txt# => 第二视图的文件# 退出第二视图,回到第一视图exit# 第一视图看不到ls/mnt/room2# => (没有任何输出)cat/mnt/room2/secret.txt# => 报错:没有那个文件或目录第二视图的新挂载只写在自己的清单上,第一视图的清单没有这条记录。
⚠️ 常见错误
错误:以为 exit 后文件还在
exit只是退出视图,但挂载的房间还在内存里。如果同一目录在宿主机会被用到,可能冲突。
解决:退出前卸载
在第二视图里执行 终端:umount/mnt/room2exit⚠️ 安全警告
第二视图里/仍然是宿主机的根文件系统(只是挂载表不同)。不要执行rm -rf /等危险命令,会删除宿主机文件。
注意:清单隔离 ≠ 文件隔离
如果第二视图修改了已经存在的文件(如
/etc/hosts),第一视图能看到变化,因为底层文件数据是同一个。只有新挂载的房间才是隔离的。
验证:
# 在第二视图里echo"test">>/etc/hosts# 退出到第一视图exitcat/etc/hosts# => 能看到 "test" 被追加进去了四、第三视图:在独立清单里再复制一份
4.1 怎么创建?
在第二视图内部执行(在第二视图的 shell 里继续输入):
# 确保你在第二视图里(提示符可能变了,或者你记得执行过 unshare)sudounshare--mount--propagationprivate /bin/bash# 在第二视图里再复制一份清单,创建第三视图mkdir/mnt/room2/room3mount-ttmpfs tmpfs /mnt/room2/room3echo"第三视图的文件">/mnt/room2/room3/deep.txt“在内部执行” = 基于第二视图的清单再复制一份,作为第三视图的起点。
4.2 即时验证
# 第三视图能看到所有内容cat/mnt/room2/secret.txt# 第二视图的文件(复制时带过来的)cat/mnt/room2/room3/deep.txt# 第三视图的文件(自己的新挂载)# 退出到第二视图exitcat/mnt/room2/secret.txt# ✅ 能看到cat/mnt/room2/room3/deep.txt# ❌ 报错!第二视图看不到第三视图的新挂载# 退出到第一视图exitcat/mnt/room2/secret.txt# ❌ 报错!第一视图看不到第二视图的新挂载4.3 三个视图对比
| 查看内容 | 第一视图 | 第二视图 | 第三视图 |
|---|---|---|---|
/mnt/room2/secret.txt | ❌ 看不到 | ✅ 能看到 | ✅ 能看到(复制时带过来的) |
/mnt/room2/room3/deep.txt | ❌ 看不到 | ❌ 看不到 | ✅ 能看到 |
修改/etc/hosts(已存在文件) | ✅ 能看到变化 | ✅ 能看到变化 | ✅ 能看到变化(底层共享) |
规律:每多一个视图,就多一层清单隔离。外层看不到内层的新挂载,除非显式进入内层。
⚠️ 常见错误
错误:在第二视图外创建第三视图
如果你在第一视图里执行sudo unshare --mount,创建的是另一个第二视图,不是第三视图。第三视图必须在第二视图内部创建。
五、从视图到容器:换房间
5.1 问题:前面的视图只隔离了清单,没换房间
第二视图、第三视图的清单虽然独立了,但根目录/里的内容还是和第一视图一样。/bin/bash、/etc/passwd都是宿主机的文件。
容器需要:不仅清单独立,根目录/也要换成新房间。
5.2 容器怎么做?(以文件系统隔离为例)
dockerrun-itubuntu /bin/bash内部三步:
- 创建 Mount Namespace(第二视图,清单隔离)
- 挂载容器镜像文件系统(新房间,不是第三视图)
- 用
pivot_root把根目录/指向新房间(换房间操作)
pivot_root:把进程的根目录切换到指定目录。这是容器运行时的内部机制,实验 1-4 不手动操作,只理解原理。
容器里:
ls/# => bin boot dev etc home lib ...(这是 Ubuntu 镜像的内容,不是宿主机的)cat/etc/os-release# => PRETTY_NAME="Ubuntu 22.04 LTS"宿主机上:
终端:cat/etc/os-release=>显示宿主机自己的系统(如 CentOS)5.3 容器的文件系统隔离
容器的文件系统隔离 = 第二视图(清单隔离)+ 新房间 + 换根容器比纯 Namespace 多了一层:不仅"换清单"(视图隔离),还"换房间"(根目录替换)。
注意:容器没有创建"第三视图",它复用了第二视图的 Mount Namespace,在里面直接挂载新房间并换根。
完整的容器还需要 PID Namespace(进程隔离)、Network Namespace(网络隔离)、User Namespace(用户权限隔离)等。这里只讨论文件系统隔离。
5.4 安全 implications(文件系统隔离机制)
| 攻击场景 | 文件系统隔离机制(标准容器) |
|---|---|
容器内读取宿主机/etc/shadow | ✅ 换根后看不到 |
| 容器内修改宿主机文件 | ✅ 清单隔离保护 |
注意:通过 volume 显式挂载宿主机目录,容器可以访问。这是配置行为,不是隔离机制的绕过。
实验 1-4 的理解适用于标准容器。特权容器(
--privileged)或内核漏洞逃逸需要额外防护,不在本文范围。
六、核心结论
第一视图(宿主机) │ ├── 房间清单:/bin, /etc, /home, /mnt...(所有系统挂载) │ ▼ unshare --mount(复制清单) 第二视图(容器边界) │ ├── 复制了第一视图的清单作为起点 ├── 之后独立添加新房间 ├── 第一视图看不到第二视图的新房间 │ ▼ 内部再 unshare + mount(再复制清单) 第三视图(嵌套隔离) │ ├── 在第二视图内部再创建的 Mount Namespace ├── 第二视图看不到第三视图的新房间 ├── 底层还是同一个内核 │ └── 宿主机不进入第二视图,永远看不到这里 容器(第二视图的应用) │ ├── 用第二视图的 Mount Namespace 做边界 ├── 在内部挂载新房间(容器镜像) ├── 用 pivot_root 把根目录指向新房间 │ └── 不是第三视图,是"第二视图 + 换房间"- 同一内核,不同房间清单。
- 跨视图不可见,必须显式进入。
- 容器多一步,不仅换清单,还换房间。
七、完整实验(带清理)
# === 步骤1:创建第二视图,记录 PID ===sudounshare--mount--propagationprivate /bin/bashecho$$# 记下这个 PID,如 1234readlink /proc/$$/ns/mount# => mnt:[...](记下这个数字)# === 步骤2:第二视图挂载 ===mkdir/mnt/room2mount-ttmpfs tmpfs /mnt/room2echo"第二视图">/mnt/room2/a.txt# === 步骤3:在第二视图里创建第三视图 ===sudounshare--mount--propagationprivate /bin/bash readlink /proc/$$/ns/mount# => mnt:[...](数字与步骤1不同)# === 步骤4:第三视图挂载(嵌套在room2内部) ===mkdir/mnt/room2/room3mount-ttmpfs tmpfs /mnt/room2/room3echo"第三视图">/mnt/room2/room3/b.txt# === 步骤5:第三视图验证(能看到所有) ===cat/mnt/room2/a.txt# 第二视图的文件cat/mnt/room2/room3/b.txt# 第三视图的文件# === 步骤5.5:在第三视图内先卸载内层(必须在exit之前!)===umount/mnt/room2/room3# === 步骤6:回退到第二视图 ===exitcat/mnt/room2/a.txt# ✅ 能看到cat/mnt/room2/room3/b.txt# ❌ 报错(目录存在但为空,因为room3已卸载)# === 步骤6.5:用 nsenter 进入第二视图验证 ===# 需要另开终端窗口执行,保持原终端不退出:# 1. 保持原终端窗口不关闭(第二视图的bash还在运行)# 2. 新开一个终端窗口(或新标签页)# 3. 在新终端里执行以下命令# 4. 验证完成后在新终端里输入 exit 退出# 5. 回到原终端继续步骤7sudonsenter-t1234-m/bin/bashcat/mnt/room2/a.txt# ✅ 能看到cat/mnt/room2/room3/b.txt# ❌ 报错(确认第二视图看不到第三视图)exit# 回到原终端# === 步骤7:在第二视图内卸载外层 ===umount/mnt/room2# 在第二视图内卸载exit# 回退到第一视图# === 步骤8:第一视图验证(看不到第二视图) ===cat/mnt/room2/a.txt# ❌ 报错# === 步骤9:确认回到第一视图 ===readlink /proc/$$/ns/mount# 与步骤1的数字对比,确认一致八、清理检查(实验后必做)
# 检查是否还有残留挂载mount|grep/mnt/room# => 应该没有任何输出# 检查 room2 目录是否为空(umount 后)ls/mnt/room2# => (没有任何输出,目录存在但为空)# 检查 room3 目录是否消失(随 room2 的 tmpfs 一起释放)ls/mnt/room2/room3# => 报错:没有那个文件或目录# 如果有残留,手动卸载sudoumount/mnt/room2/room3sudoumount/mnt/room2# 删除空目录sudormdir/mnt/room2/room3sudormdir/mnt/room2附录 A:故障排查
| 现象 | 原因 | 解决 |
|---|---|---|
umount: not mounted | 在当前视图里没有这条挂载记录 | 确认你在正确的视图里,或检查 `mount |
nsenter: cannot open /proc/1234/ns/mount | PID 对应的进程已退出 | 重新执行步骤 1-2,确保第二视图的 bash 没退出 |
ls /mnt/room2显示文件 | 可能没执行 umount,或 mount 传播到宿主机 | 检查 `mount |
修改/etc/hosts后宿主机也变了 | 这是正常的,清单隔离 ≠ 文件隔离 | 理解"已存在文件是共享的" |
附录 B:后续学习
实验 1-4 理解了 Mount Namespace(文件系统视图隔离)。后续内容:
- User Namespace:用户权限隔离(容器内的 root 映射到宿主机普通用户)
附录 C:延伸阅读
man 2 unshare:创建新 Namespace 的系统调用man 1 nsenter:进入指定 Namespace 的命令man 2 pivot_root:切换根目录的系统调用- Linux 内核源码
fs/namespace.c:Mount Namespace 实现
