一个常见场景
在一台装了 Docker 的 Linux 服务器上,某个普通用户想改配置、重启服务,却发现自己没有任何提权手段:
$ sudo systemctl reload nginxuser is not in the sudoers file.
$ su -su: Authentication failuresudo 用不了(不在 sudoers),su - 也进不去(root 密码没设或不对)。看起来是死局。
但如果 id 的输出里出现了 docker 组:
$ iduid=1000(user) gid=1000(user) groups=1000(user),993(docker)那么这个用户其实已经拥有等价于 root 的权限。
核心结论:在一台安装了 Docker 的机器上,
docker组的成员等价于 root。 这不是漏洞,是 Docker 的设计特性。也正因如此,它是运维和安全审计中必须重视的一点。
一、为什么 docker 组 = root
Docker 采用 客户端 / 守护进程 分离架构:
- 后台有一个常驻守护进程
dockerd,以 root 身份运行; - 命令行里的
docker只是客户端,它通过一个 Unix socket 把指令发给dockerd执行。
这个 socket 是 /var/run/docker.sock,属主组正是 docker:
$ ls -l /var/run/docker.socksrw-rw---- 1 root docker 0 ... /var/run/docker.sock于是形成这样一条链:
用户在 docker 组 → 有权读写 docker.sock → 能给 root 身份的 dockerd 下命令 → dockerd 以 root 执行任意容器操作 → 效果等价于 root用户自己不是 root,但能指挥一个 root 进程。而”能指挥 root 进程做任意事”与”自己就是 root”没有本质区别——例如让它启动一个容器,把宿主机整个文件系统挂进去,再以容器内 root 读写。
这就是为什么 su、sudo 全部拒绝,该用户依然有路。
二、命名空间:容器隔离的本质
要理解提权命令,先要理解容器为什么”看不到”宿主机。
容器不是虚拟机。 它和宿主机共用同一个内核。所谓”隔离”,是内核用 namespace(命名空间) 给进程制造的一种”视觉欺骗”:
| namespace 类型 | 隔离的内容 |
|---|---|
| mount | 文件系统视图(容器看到的 /etc、/usr 是自己的一套) |
| pid | 进程视图(容器只看到自己的进程,自己的 PID 1) |
| network | 网络栈(独立网卡、IP、端口) |
| user | 用户与 UID 映射(容器里的 root 可映射为宿主机普通用户) |
| uts / ipc | 主机名、进程间通信等 |
容器之所以”独立”,只是因为内核让它的进程只能看到属于自己那套 namespace 的东西。这堵墙是虚的,是内核给进程戴上的一副”只能看到局部”的眼镜。
提权的思路,就是把这副眼镜摘掉。
三、提权命令逐字拆解
docker run -it --rm --privileged --pid=host --net=host justincormack/nsenter1 /bin/sh| 部分 | 作用 |
|---|---|
docker run | 创建并启动一个新容器 |
-it | -i 保持标准输入打开,-t 分配伪终端;合起来 = 一个可交互的 shell |
--rm | 容器退出后自动删除,用完即弃 |
--privileged | 关键开关:特权模式,去掉容器几乎所有安全限制(capabilities 全给、可访问宿主机设备)。等于拆掉笼子 |
--pid=host | 共享宿主机 PID 命名空间,容器内能看到并操作宿主机全部进程。这是 nsenter 能”跳进”宿主机的前提 |
--net=host | 共享宿主机网络(此处非重点,同理是打通隔离) |
justincormack/nsenter1 | 极简镜像,启动即执行 nsenter,钻进宿主机 PID 1 所在的全部 namespace |
/bin/sh | 进去后开一个 shell |
核心机制:nsenter(enter namespace) 配合 --pid=host,做的事是:找到宿主机 PID 1(init 进程),进入它所属的全部 namespace。一旦进去,看到的 /etc/group、/etc/sudoers、所有进程,就全是宿主机的真身。
进去后提示符类似:
sh-4.2#结尾的 # 代表当前是 root,而且是宿主机的 root。
一句话:
--privileged --pid=host拆掉隔离,nsenter走进宿主机命名空间,docker保证这一切以 root 执行。
四、拿到 root 后:把用户加进管理员组
usermod -aG wheel <用户名>| 部分 | 作用 |
|---|---|
usermod | modify user,修改用户属性 |
-a | append,追加。极其重要 |
-G wheel | 目标附加组是 wheel |
-a 为什么关键? 只写 -G(不带 -a)会用新列表覆盖用户现有的所有附加组。也就是说 usermod -G wheel <用户名> 会把该用户踢出 docker 组及其他所有组,只剩 wheel。-aG 连用是必须养成的安全习惯。
wheel 是什么? 一个历史悠久、约定俗成的”管理员组”。在 RHEL / CentOS / 阿里云 Linux 系里,/etc/sudoers 默认有一行:
%wheel ALL=(ALL) ALL% 表示这是组规则,含义是”wheel 组成员可对任何主机、以任何身份、执行任何命令”——即完整 sudo 权限。
发行版差异: Debian / Ubuntu 系用的是
sudo组,不是wheel。动手前先确认发行版:Terminal window . /etc/os-release && echo "$ID / $ID_LIKE"RHEL 系 → 加
wheel;Debian 系 → 加sudo。
确认 wheel 规则确实启用(行首无 #):
grep -E '^\s*%wheel' /etc/sudoers五、一个坑:nscd 报错不影响结果
执行 usermod 时,某些环境下可能看到一堆报错:
nscd: relocation error: nscd: symbol __copy_grp, version GLIBC_PRIVATE not defined in file libc.so.6 with link time reference不必慌张。 usermod 做两件事:① 改 /etc/group 文件;② 通知 nscd(名字缓存服务)刷新缓存。报错出在第二步——在 nsenter 拼出来的环境里,nscd 二进制和它加载的 libc.so.6 来自不同 glibc 版本,符号对不上,通知崩了。
但改文件和刷缓存是两件独立的事,文件已经写成功。验证一下:
$ grep wheel /etc/groupwheel:x:10:<用户名>用户名出现在 wheel 行末尾,就说明改动已落盘,那堆报错无害。
提示:这个报错也说明 nsenter 环境的 libc 上下文不干净。改动确认成功后应尽快退出容器,后续操作(设 root 密码、编辑配置)都回到干净的原生 SSH 环境里做,避免在脏环境中累积风险。
六、为什么必须重新登录才生效
改完组,当前会话里 id 仍看不到 wheel。原因:
用户的组信息是在登录那一刻由系统读取、写入会话进程凭证的,之后不会自动刷新。
文件 /etc/group 已经改了,但当前 SSH 会话的进程还持有旧的组列表。必须退出、重新登录,新会话重新读取文件,wheel 才进入凭证。这与改环境变量后需要重新 source 是同一个道理。
exit # 退出 nsenter shellexit # 退出当前 SSH 会话# 重新 SSH 登录验证:
$ iduid=1000(user) gid=1000(user) groups=1000(user),10(wheel),993(docker)
$ sudo systemctl reload nginx # 通了七、完整操作清单
# ── 0. 确认在 docker 组 ──id# groups=...,993(docker) ← 有这个即可往下
# ── 1. 借 docker 组进入宿主机 root shell ──docker run -it --rm --privileged --pid=host --net=host \ justincormack/nsenter1 /bin/sh# 镜像拉不下来时的兜底(用本地 alpine chroot 进宿主机根):# docker run -it --rm --privileged --pid=host -v /:/host alpine chroot /host bash
# ── 2. 在宿主机 root 环境里操作 ──usermod -aG wheel <用户名> # RHEL/阿里云系;Debian 系换成 sudo 组grep -E '^\s*%wheel' /etc/sudoers # 确认规则启用(行首无 #)grep wheel /etc/group # 确认写盘成功passwd root # 顺手设 root 密码,多一条退路
# ── 3. 退出并重新登录 ──exit; exit# 重新 SSH,然后:idsudo systemctl reload nginx八、安全启示:docker 组是个”隐形 root”
本文最值得记住的,不是”怎么提权”,而是它揭示的风险:
- 任何进了
docker组的账户,都能无声无息拿到宿主机 root。 - 权限审计时,不能把 docker 组成员当成”受限普通用户”——它实质上是 root。
- 多人共用的服务器上,给某人加 docker 组 ≈ 给了 sudo,而且没有 sudo 那样的日志和命令白名单约束。
加固方向
-
Rootless Docker 让守护进程以非 root 用户运行,从根上消除”docker 组 = root”。是官方推荐的现代方案。 参考:
https://docs.docker.com/engine/security/rootless/ -
收紧 docker 组成员 只给真正需要管理容器的人加组,并在制度上明确”进 docker 组 = 授予 root 等级权限”。
-
用 sudo 白名单替代直接给组 如果某人只需要重启某个服务,与其塞进 docker/wheel 组,不如在 sudoers 里放行特定命令:
<用户名> ALL=(root) NOPASSWD: /usr/sbin/nginx -t, /usr/sbin/nginx -s reload这样既能干活,又不给完整 root,而且每次执行都有日志。
-
审计现有 docker 组成员
Terminal window getent group docker定期检查这行里有谁,清理不再需要的账户。
附:关于那句吓人的警告
This incident will be reported.这只是 sudo 在越权时的标准文案,会把这次失败记入日志(通常 syslog,或发邮件给管理员),对操作者没有实际影响,不代表”出事了”。看到不必慌。
docker 组等价于 root 是 Docker 官方文档明确说明的已知特性。理解其原理的目的是据此做好防护,相关操作应仅在自己有权管理的服务器上进行。