Privileged 权限:你的容器真的需要吗?
Privileged 权限:你的容器真的需要吗?
实验环境:Ubuntu 24.04 / 内核 6.8.0-106-generic / Cgroup v2 / Docker 29.1.3 / 华为云 FlexusX 8C16G(8vCPU/16G)。
全程仅做「只读读取宿主文件」「挂载内存盘」等安全演示,未对宿主做任何写破坏;演示完即卸载/退出。
docker run --privileged是运维中最常见的「图省事」写法:网络不通加 --privileged,权限不够加 --privileged,mount 失败还是 --privileged。它确实「一加就好」,但代价是把整台宿主机的钥匙交给了容器。本文用一组对照实验,把 --privileged 到底给了什么、危险在哪、以及最小权限怎么给,一次讲清。
1. 引子:一个「加 privileged 就好」的事故
某次排查容器里配静态路由失败,同事甩手一句「加--privileged试试」——命令一贴,路由配通了,但容器同时也拿到了宿主全部设备的读写权。如果这容器镜像来源不可控,或者跑的是第三方代码,这就是一条现成的逃逸路径。
关键认知:–privileged 不是「多一点权限」,而是「放弃容器边界」。下面用实验证明。
2. 复现一:普通容器里哪些操作被拒
新建一个普通容器(无任何特权参数),尝试三件「敏感操作」:
# (1) 创建网络接口 -> 需要 CAP_NET_ADMIN$dockerrun--rmbusyboxiplinkadddummy0typedummy ip: RTNETLINK answers: Operation not permitted# (2) 挂载文件系统 -> 需要 CAP_SYS_ADMIN$dockerrun--rmbusyboxsh-c"mount -t tmpfs tmpfs /mnt"mount: permission denied(are you root?)# busybox 的通用提示,本质是缺 CAP_SYS_ADMIN# (3) 修改系统时间 -> 需要 CAP_SYS_TIME$dockerrun--rmbusyboxsh-c"date -s\"2020-01-01 00:00:00\""date: can'tsetdate: Operation not permitted三类操作分别对应CAP_NET_ADMIN/CAP_SYS_ADMIN/CAP_SYS_TIME,普通容器默认都没有,于是被内核干净地拒绝。这是预期且正确的行为——容器不该能改宿主时间、不该能挂宿主文件系统。
3. 复现二:–privileged 一把梭,全部放行
同一个容器,只加--privileged:
$dockerrun--privileged--rmbusyboxsh-c" ip link add dummy0 type dummy && echo LINK_ADD_OK mount -t tmpfs tmpfs /mnt && echo MOUNT_OK date -s\"2020-01-01 00:00:00\">/dev/null 2>&1 && echo DATE_SET_OK ip link del dummy0 2>/dev/null; umount /mnt 2>/dev/null"LINK_ADD_OK MOUNT_OK DATE_SET_OK一句话:–privileged 把 ip/mount/date 全放开了。它等价于「给全部 capability + 关闭 seccomp + 关闭 AppArmor + 共享所有宿主设备」。 convenient,但也意味着容器此时几乎和宿主机 root 平起平坐。
4. 复现三:–privileged 的危险——看得见宿主磁盘
4.1 /dev 可见性对比
$echo-n"普通容器可见 /dev 条目数: ";dockerrun--rmbusyboxsh-c"ls /dev | wc -l"普通容器可见 /dev 条目数:15$echo-n"特权容器可见 /dev 条目数: ";dockerrun--rm--privilegedbusyboxsh-c"ls /dev | wc -l"特权容器可见 /dev 条目数:186$dockerrun--rm--privilegedbusyboxls/dev/vda* /dev/vda /dev/vda1# 宿主的 root 磁盘设备,普通容器里根本看不到普通容器只看到 15 个经过过滤的设备节点;特权容器直接看到宿主的整块系统盘/dev/vda1。
4.2 逃逸演示:挂载宿主根盘、读到 /etc/shadow
最直白的逃逸(只读挂载、仅读取,不破坏宿主):
# 方式A:特权 + 卷挂载把宿主根目录挂进容器$dockerrun--privileged--rm-v/:/host busyboxsh-c" head -2 /host/etc/shadow ls -d /host/root"root:$6$U1lmc3ta$/fRHfNApxwF6bxTMA7LRzWXuKx9jIavlRafFOHIYpW.AINDkf9OESrN.ZTkVIw0PLqkvYA002fgL5SfbBYoHA.:20657:0:99999:7::: daemon:*:19836:0:99999:7::: /host/root# 宿主的 /root 目录在容器里完全可见可写容器里直接读到了宿主的/etc/shadow,并看到了宿主/root。一旦镜像或应用代码不可信,这就是完整的「容器→宿主」逃逸:能改宿主/etc/shadow、能写宿主crontab、能植入持久化后门。
真实攻击链往往更简单:特权容器 +
nsenter -t 1 -m -u -n -i sh直接进宿主 PID 1 的命名空间;或挂载宿主磁盘后改/etc/cron.d。本文只演示读取,点到为止。
5. 原理剖析:–privileged 到底改了什么
容器「权限」由三层独立机制共同决定,–privileged 把这三层全部打开:
5.1 Capabilities(能力位图)
Linux 把传统 root 的「超级权」拆成 ~40 个细粒度 capability。进程的有效能力写在/proc/<pid>/status的CapEff字段(十六进制位图)。
# 普通容器$dockerrun--rmbusyboxsh-c"grep CapEff /proc/self/status"CapEff: 00000000a80425fb# 特权容器$dockerrun--rm--privilegedbusyboxsh-c"grep CapEff /proc/self/status"CapEff: 000001ffffffffff# 全部位为 1 = 拥有所有 capability# 解码普通容器的位图,看 Docker 默认给了哪些$NHEX=00000000a80425fb $ capsh--decode=$NHEX0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill, cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw, cap_sys_chroot,cap_mknod,cap_audit_write,cap_setfcap对比宿主自身(root 全开):
$ capsh--print|head-2Current:=ep Boundingset=cap_chown,cap_dac_override,...,cap_bpf,cap_checkpoint_restore# 全 40+ 项$grepCapEff /proc/self/status CapEff: 000001ffffffffff结论:普通容器只拿了 14 个相对安全的默认 capability;–privileged 一次性补齐到全部。问题就出在「补齐的全集」里包含cap_sys_admin、cap_sys_ptrace、cap_sys_module等高危项——它们单独任何一个都足以威胁宿主。
5.2 seccomp(系统调用过滤)
Docker 默认给每个容器套一个 seccomp 白名单,禁止一批危险 syscall(如mount、ptrace、kexec_load、bpf部分等),即使进程有对应 capability 也会被拦。验证:
# 只给 CAP_SYS_ADMIN,但默认 seccomp 仍拦 mount$dockerrun --cap-add SYS_ADMIN--rmbusyboxsh-c"mkdir -p /mnt; mount -t tmpfs tmpfs /mnt"mount: mounting tmpfs on /mnt failed: Permission denied# 仍然不行# 关掉 seccomp 还是不行 —— 因为还有 AppArmor$dockerrun --cap-add SYS_ADMIN --security-optseccomp=unconfined--rmbusyboxsh-c"..."mount:... Permission denied# 三个闸门全开,mount 才成功$dockerrun --cap-add SYS_ADMIN --security-optseccomp=unconfined\--security-optapparmor=unconfined--rmbusyboxsh-c"mkdir -p /mnt; mount -t tmpfs tmpfs /mnt && echo MOUNT_YES"MOUNT_YES这条链路是本文最重要的结论:capability 是「必要不充分」条件;seccomp 和 AppArmor 是另外两道独立闸门。--privileged同时关掉三道(全 capability + seccomp=unconfined + apparmor=unconfined),所以「什么都能干」。
5.3 AppArmor(MAC 强制访问控制)
Ubuntu 上 Docker 默认套docker-defaultAppArmor profile,它对 mount、文件写等做了 deny 规则。上面实验已证明:即便给了 SYS_ADMIN 且关了 seccomp,只要 AppArmor 还在,mount仍被拒——必须--security-opt apparmor=unconfined才放行。
6. 方案:最小权限(Least Privilege)怎么给
核心原则:不要 --privileged;只通过--cap-add给恰好够用的那一项 capability,并保持 seccomp/AppArmor 默认开启。
6.1 实战:只给 NET_ADMIN
$echo-n"仅加 NET_ADMIN 能否 ip link add: "$dockerrun --cap-add NET_ADMIN--rmbusyboxsh-c"ip link add dummy0 type dummy && echo YES || echo NO"YES# 网络管理权限到位,操作成功$echo-n"仅加 NET_ADMIN 能否 mount(应仍不行): "$dockerrun --cap-add NET_ADMIN--rmbusyboxsh-c"mkdir -p /mnt; mount -t tmpfs tmpfs /mnt 2>&1 && echo YES || echo NO"NO# mount 依然被拒 —— 因为没给 SYS_ADMIN,也没放开 seccomp/AppArmor只加NET_ADMIN,容器能管网络,但依然无法挂载、看不到宿主磁盘——这正是「够用就好」的状态。
6.2 Docker 默认 capability 与常见场景对照表
| 需求场景 | 所需 capability | Docker 默认就有? | 怎么给 |
|---|---|---|---|
| 绑定 80/443 等特权端口 | cap_net_bind_service | ✅ 默认有 | 无需额外操作 |
ping/ 原始套接字 | cap_net_raw | ✅ 默认有 | 无需额外操作 |
chown改文件属主 | cap_chown | ✅ 默认有 | 无需额外操作 |
chroot | cap_sys_chroot | ✅ 默认有 | 无需额外操作 |
| 创建/删除网络接口、iptables | cap_net_admin | ❌ | --cap-add NET_ADMIN |
| 改系统时间 | cap_sys_time | ❌ | --cap-add SYS_TIME |
| 加载内核模块 | cap_sys_module | ❌ | --cap-add SYS_MODULE(极危险,慎用) |
| 挂载文件系统 | cap_sys_admin+ 放开 seccomp + AppArmor | ❌ | 见下,尽量避开 |
| 改文件能力位 | cap_setfcap | ✅ 默认有 | 无需额外操作 |
关于「挂载」:绝大多数「容器里要 mount」的需求,正确解法不是给 SYS_ADMIN,而是:
- 需要宿主机目录 → 用
-v /host/path:/container/path卷挂载(宿主机侧控制好权限);- 需要特殊文件系统 → 在宿主侧挂好,再 bind 进容器;
- 实在要在容器内 mount(如某些 CSI 插件),才考虑
cap_sys_admin+ 自定义 seccomp/AppArmor,且必须配合只读、非特权镜像、受信任代码。
6.3 排查清单:判断一个容器「权限是否过大」
docker inspect <c> --format '{{.HostConfig.Privileged}}'是否 true —— 是则立即告警。docker inspect <c> --format '{{.HostConfig.CapAdd}}'看额外加了哪些 cap,逐个核对是否必需。docker inspect <c> --format '{{.HostConfig.SecurityOpt}}'看 seccomp/AppArmor 是否被 unconfined。- 进容器
grep CapEff /proc/self/status拿到位图,capsh --decode=<hex>解码,确认没有sys_admin/sys_module/sys_ptrace等高危项。 - 检查是否挂了宿主根或敏感目录(
-v /:/x、-v /etc:/x)。
7. 总结
- 普通容器里
ip link add/mount/date -s被拒,是因为缺对应 capability(NET_ADMIN / SYS_ADMIN / SYS_TIME)。 --privileged一次性给全部 capability + 关闭 seccomp + 关闭 AppArmor + 共享宿主所有设备,容器因此能看到宿主磁盘/dev/vda1、能挂载并读到宿主/etc/shadow——典型的逃逸路径。- 权限是三层闸门:capabilities(必要不充分)、seccomp(拦危险 syscall)、AppArmor(MAC 限制)。三者任一在,mount 等高危操作仍会被挡;–privileged 把三者全开。
- 正确做法:绝不 --privileged,用
--cap-add <单一cap>给最小权限;挂载需求优先用卷挂载而非 SYS_ADMIN。 - 排查靠
docker inspect的 Privileged/CapAdd/SecurityOpt 字段,以及容器内CapEff位图解码。
8. 思考题
- 你的 CICD 里有没有
--privileged的容器?列出它们「到底需要哪一项能力」,能否改成--cap-add? - 既然 seccomp 默认就拦了
mount,为什么很多「提权」文章仍强调「禁止 --privileged」?它的额外风险(如cap_sys_ptrace+nsenter)是什么? cap_sys_admin被称为「小特权」,它单独就能做哪些危害宿主的事(不止 mount)?- 如果业务真的必须容器内 mount(如存储插件),你认为「SYS_ADMIN + 自定义 seccomp + 非特权镜像 + 只读根文件系统」这套组合,比 --privileged 安全在哪?
下一篇:《容器中不用 root 运行程序》—— 我们将实测 root 容器在挂载目录上制造的属主混乱,并演示 DockerfileUSER、UID 映射与userns-remap三种「降权」方案。
