ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Linux User Namespace 与非特权容器:免 root 实现安全进程隔离

Linux User Namespace 与非特权容器:免 root 实现安全进程隔离 在云计算与多租户容器安全领域传统 Docker“以宿主机 root 权限运行守护进程”的架构长期被安全团队视作高危阿喀琉斯之踵。一旦容器内的恶意代码利用未修补的内核漏洞或运行时逃逸缺陷如经典的 runc CVE-2019-5736由于容器内的 UID 0 直接等价于宿主机的 UID 0攻击者逃逸后将直接接管整台物理机拥有擦除磁盘与窃取一切租户机密的至高权力。为了从根本上消除这一原罪以 Podman、LXC 和 Rootless Docker 为代表的“非特权容器Rootless Containers”迅速成为现代容器基础架构的黄金标准。而这一变革背后的物理支柱正是 Linux 众多 Namespace 中机制最为复杂、安全影响最深远的User Namespace用户命名空间。User Namespace 拥有两大不可思议的内核特性它是所有 Namespace 中唯一允许非特权普通用户无需sudo、无CAP_SYS_ADMIN直接创建的命名空间它将 UID/GID 与 Linux Capabilities 的特权概念从“系统全局绝对值”彻底解构为“命名空间局域相对值”。UID/GID 映射拓扑与 Capabilities 作用域边界理解 User Namespace 的核心在于看透内核如何通过映射表Mapping Table在两个世界之间转换身份。------------------------------------------------------------------------- | Host Root User Namespace (Global Scope) | | | | Actual Unprivileged User: | | - UID: 10001 (alice) | | - GID: 10001 (developers) | | - Global Capabilities: None (Cannot mount, cannot configure net) | ------------------------------------------------------------------------- | CLONE_NEWUSER | uid_map: 0 10001 1 (Created by alice!) | gid_map: 0 10001 1 v ------------------------------------------------------------------------- | Container User Namespace (Local Scope) | | | | Virtual Superuser: | | - Local UID: 0 (root) | | - Local GID: 0 (root) | | - Local Capabilities: Full (CAP_SYS_ADMIN, CAP_NET_ADMIN, CAP_CHOWN) | | | | Permitted Actions Inside: Blocked Actions Against Host: | | - Mount isolated procfs/tmpfs - Cannot read /etc/shadow on host | | - Create sub-veth / loopback - Cannot bind to host port 1024 | | - Unshare NEWNET, NEWPID, NEWNS - Cannot access host raw block dev | -------------------------------------------------------------------------1. 虚幻的局域 UID 0如上拓扑所示通过配置uid_map: 0 10001 1内核在当前命名空间内部将宿主机的10001映射为容器内部的0。当容器内部代码调用getuid()时内核返回0。进程自以为拥有了无上权力可以顺利执行依赖 root 检查的各种工业级二进制文件。但当该进程发起的系统调用触碰到宿主机全局资源例如访问物理磁盘文件时内核 VFS 权限检查器会立刻通过映射表将其反向解析为真实的10001。即便进程执行了逃逸它在宿主机上也只是一个毫无特权的普通账户。2. Capabilities 的局部有效性Local Scope在创建 User Namespace 的瞬间内核会为该进程在其私有命名空间内赋予全套满配的 Linux Capabilities包括CAP_SYS_ADMIN、CAP_NET_ADMIN等。然而这些 Capabilities 严格受限于该 User Namespace 所拥有的对象。进程可以凭借局部的CAP_SYS_ADMIN自由挂载自己的procfs、创建自己的网络命名空间CLONE_NEWNET但绝不能利用它去挂载宿主机的真实物理分区。纯 C23手写免 root 启动完整隔离容器环境以下代码完全遵循 C23 标准由普通非特权用户编译后直接运行无需借助任何sudo提权。程序演示了如何通过创建 User Namespace 并在其支撑下派生隔离的 Mount 与 PID 空间并在沙箱内挂载专属的/proc// rootless_sandbox.c #define _GNU_SOURCE #include stdio.h #include stdlib.h #include string.h #include unistd.h #include sched.h #include signal.h #include sys/wait.h #include sys/mount.h #include fcntl.h constexpr size_t STACK_SIZE 1024 * 1024; static char child_stack[STACK_SIZE]; // 更新 UID / GID 映射关系 static void update_map(const char *mapping, const char *map_file) { int fd open(map_file, O_WRONLY); if (fd 0) { perror(Failed to open map file); exit(1); } size_t len strlen(mapping); if (write(fd, mapping, len) ! (ssize_t)len) { perror(Failed to write mapping); close(fd); exit(1); } close(fd); } // 隔离沙箱主函数 static int sandbox_main(void *arg) { int *sync_pipe (int *)arg; close(sync_pipe[1]); // 等待父进程在外部完成 uid_map 与 gid_map 写入 char ch; if (read(sync_pipe[0], ch, 1) 0) { perror(sync read failed); return 1; } close(sync_pipe[0]); printf(\n[Container] Inside Rootless Sandbox!\n); printf([Container] Effective UID: %d, GID: %d\n, getuid(), getgid()); // 此时在命名空间内已拥有局域 CAP_SYS_ADMIN可以安全挂载属于自己的虚拟文件系统 if (mount(none, /tmp, tmpfs, 0, ) 0) { printf([Container] Successfully mounted private tmpfs at /tmp without host root!\n); umount(/tmp); } else { perror([Container] mount tmpfs failed); } return 0; } int main(void) { uid_t real_uid getuid(); gid_t real_gid getgid(); printf([Host] Current non-privileged user UID: %d, GID: %d\n, real_uid, real_gid); int sync_pipe[2]; if (pipe(sync_pipe) 0) { perror(pipe failed); return 1; } // 普通用户直接调用 CLONE_NEWUSER附带 NEWNS 与 NEWPID pid_t child_pid clone(sandbox_main, child_stack STACK_SIZE, CLONE_NEWUSER | CLONE_NEWNS | CLONE_NEWPID | SIGCHLD, sync_pipe); if (child_pid 0) { perror(clone failed (Ensure kernel.unprivileged_userns_clone is 1)); return 1; } // 父进程在宿主机侧为子进程配置映射表 char map_path[128]; char map_data[128]; // 1. Linux 3.19 安全防御要求配置 gid_map 前必须先禁用 setgroups snprintf(map_path, sizeof(map_path), /proc/%d/setgroups, child_pid); int sg_fd open(map_path, O_WRONLY); if (sg_fd 0) { write(sg_fd, deny, 4); close(sg_fd); } // 2. 映射 UID: 将容器内 0 号映射为当前外部真实 UID snprintf(map_path, sizeof(map_path), /proc/%d/uid_map, child_pid); snprintf(map_data, sizeof(map_data), 0 %d 1\n, real_uid); update_map(map_data, map_path); // 3. 映射 GID: 将容器内 0 号映射为当前外部真实 GID snprintf(map_path, sizeof(map_path), /proc/%d/gid_map, child_pid); snprintf(map_data, sizeof(map_data), 0 %d 1\n, real_gid); update_map(map_data, map_path); // 通知沙箱特权映射配置完成 write(sync_pipe[1], GO, 2); close(sync_pipe[1]); waitpid(child_pid, nullptr, 0); printf([Host] Rootless container exited cleanly.\n); return 0; }生产安全与避坑指南非特权容器在带来极致安全隔离的同时也对底层文件权限系统带来了前所未有的工程挑战在实战中必须恪守以下法则1. setgroups deny 强制约束在早期 Linux 实现中普通用户可以通过 User Namespace 随意丢弃某些附属组Supplementary Groups从而绕过宿主机文件系统中基于组黑名单的安全限制。为此内核引入了强制规则非特权用户配置gid_map之前必须主动向/proc/[pid]/setgroups写入deny。如果未执行此操作直接写入gid_map内核将毫不留情地返回-EPERM。2. 多 UID 映射与 newuidmap / newgidmap上述程序只映射了单一 UID0 映射为 10001。但如果容器内部需要运行多用户服务例如容器内既有 root又有 nobody 或 mysql 用户就需要一段连续的 UID 映射区间。在标准 Linux 中非特权用户默认只能将自身 UID 写入映射表。若要映射多 UID必须依赖系统管理员在/etc/subuid中预分配从属 UID 范围并借助带有 SUID 权限的辅助工具newuidmap来安全写入映射。3. 文件属主“全部变成 nobody”的权限迷思当容器在内部创建了一个文件由于映射表外不存在该 UID 的映射当外部宿主机用户查看该文件时所有者常常显示为65534nobody:nogroup。在配置共享存储卷或挂载宿主机目录时必须利用现代内核的ID-mapped MountsID 映射挂载特性在文件系统挂载点级别做重投影彻底理顺跨租户文件所有权的物理归属。User Namespace 彻底颠覆了 Unix 诞生半个世纪以来的特权独占假设。它不仅赋予了一线普通用户自由编排容器的权力更为现代生产集群筑起了一道坚如磐石的纵深防御天堑。
返回列表