2235 字
11 分钟
docker 组即 root:一条被低估的本机提权链

一个常见场景#

在一台装了 Docker 的 Linux 服务器上,某个普通用户想改配置、重启服务,却发现自己没有任何提权手段:

Terminal window
$ sudo systemctl reload nginx
user is not in the sudoers file.
$ su -
su: Authentication failure

sudo 用不了(不在 sudoers),su - 也进不去(root 密码没设或不对)。看起来是死局。

但如果 id 的输出里出现了 docker 组:

Terminal window
$ id
uid=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

Terminal window
$ ls -l /var/run/docker.sock
srw-rw---- 1 root docker 0 ... /var/run/docker.sock

于是形成这样一条链:

用户在 docker 组
→ 有权读写 docker.sock
→ 能给 root 身份的 dockerd 下命令
→ dockerd 以 root 执行任意容器操作
→ 效果等价于 root

用户自己不是 root,但能指挥一个 root 进程。而”能指挥 root 进程做任意事”与”自己就是 root”没有本质区别——例如让它启动一个容器,把宿主机整个文件系统挂进去,再以容器内 root 读写。

这就是为什么 susudo 全部拒绝,该用户依然有路。


二、命名空间:容器隔离的本质#

要理解提权命令,先要理解容器为什么”看不到”宿主机

容器不是虚拟机。 它和宿主机共用同一个内核。所谓”隔离”,是内核用 namespace(命名空间) 给进程制造的一种”视觉欺骗”:

namespace 类型隔离的内容
mount文件系统视图(容器看到的 /etc/usr 是自己的一套)
pid进程视图(容器只看到自己的进程,自己的 PID 1)
network网络栈(独立网卡、IP、端口)
user用户与 UID 映射(容器里的 root 可映射为宿主机普通用户)
uts / ipc主机名、进程间通信等

容器之所以”独立”,只是因为内核让它的进程只能看到属于自己那套 namespace 的东西。这堵墙是虚的,是内核给进程戴上的一副”只能看到局部”的眼镜。

提权的思路,就是把这副眼镜摘掉。


三、提权命令逐字拆解#

Terminal window
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、所有进程,就全是宿主机的真身

进去后提示符类似:

Terminal window
sh-4.2#

结尾的 # 代表当前是 root,而且是宿主机的 root

一句话:--privileged --pid=host 拆掉隔离,nsenter 走进宿主机命名空间,docker 保证这一切以 root 执行。


四、拿到 root 后:把用户加进管理员组#

Terminal window
usermod -aG wheel <用户名>
部分作用
usermodmodify user,修改用户属性
-aappend,追加。极其重要
-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 规则确实启用(行首无 #):

Terminal window
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 版本,符号对不上,通知崩了。

改文件和刷缓存是两件独立的事,文件已经写成功。验证一下:

Terminal window
$ grep wheel /etc/group
wheel:x:10:<用户名>

用户名出现在 wheel 行末尾,就说明改动已落盘,那堆报错无害。

提示:这个报错也说明 nsenter 环境的 libc 上下文不干净。改动确认成功后应尽快退出容器,后续操作(设 root 密码、编辑配置)都回到干净的原生 SSH 环境里做,避免在脏环境中累积风险。


六、为什么必须重新登录才生效#

改完组,当前会话里 id 仍看不到 wheel。原因:

用户的组信息是在登录那一刻由系统读取、写入会话进程凭证的,之后不会自动刷新。

文件 /etc/group 已经改了,但当前 SSH 会话的进程还持有旧的组列表。必须退出、重新登录,新会话重新读取文件,wheel 才进入凭证。这与改环境变量后需要重新 source 是同一个道理。

Terminal window
exit # 退出 nsenter shell
exit # 退出当前 SSH 会话
# 重新 SSH 登录

验证:

Terminal window
$ id
uid=1000(user) gid=1000(user) groups=1000(user),10(wheel),993(docker)
$ sudo systemctl reload nginx # 通了

七、完整操作清单#

Terminal window
# ── 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,然后:
id
sudo systemctl reload nginx

八、安全启示:docker 组是个”隐形 root”#

本文最值得记住的,不是”怎么提权”,而是它揭示的风险

  • 任何进了 docker 组的账户,都能无声无息拿到宿主机 root。
  • 权限审计时,不能把 docker 组成员当成”受限普通用户”——它实质上是 root。
  • 多人共用的服务器上,给某人加 docker 组 ≈ 给了 sudo,而且没有 sudo 那样的日志和命令白名单约束。

加固方向#

  1. Rootless Docker 让守护进程以非 root 用户运行,从根上消除”docker 组 = root”。是官方推荐的现代方案。 参考:https://docs.docker.com/engine/security/rootless/

  2. 收紧 docker 组成员 只给真正需要管理容器的人加组,并在制度上明确”进 docker 组 = 授予 root 等级权限”。

  3. 用 sudo 白名单替代直接给组 如果某人只需要重启某个服务,与其塞进 docker/wheel 组,不如在 sudoers 里放行特定命令

    <用户名> ALL=(root) NOPASSWD: /usr/sbin/nginx -t, /usr/sbin/nginx -s reload

    这样既能干活,又不给完整 root,而且每次执行都有日志。

  4. 审计现有 docker 组成员

    Terminal window
    getent group docker

    定期检查这行里有谁,清理不再需要的账户。


附:关于那句吓人的警告#

This incident will be reported.

这只是 sudo 在越权时的标准文案,会把这次失败记入日志(通常 syslog,或发邮件给管理员),对操作者没有实际影响,不代表”出事了”。看到不必慌。


docker 组等价于 root 是 Docker 官方文档明确说明的已知特性。理解其原理的目的是据此做好防护,相关操作应仅在自己有权管理的服务器上进行。

docker 组即 root:一条被低估的本机提权链
https://blog.cuixu.cn/posts/devops/docker-group-equals-root/
作者
崔旭
发布于
2026-07-20
许可协议
CC BY-NC-SA 4.0