Docker核心技术之Capabilities
04 · Capabilities:细粒度权限
重要程度:★★★★☆ 重要 | 适用角色:SRE / 安全 | 前置:01 Namespace
一、为什么必须学
坑 1:容器里是 root,却连文件都改不了
docker exec <c> touch /some/file
# touch: cannot touch '/some/file': Permission denied
容器内 id 显示 uid=0(root),但很多操作被拒。"容器内 root" 和 "宿主 root" 是两个完全不同的概念。 不懂 capabilities,看到 root 就会认为"什么都能干"。
坑 2:为了解决问题上了 --privileged,然后全网裸奔
docker run --privileged ... # 容器获得宿主几乎全部权限
很多人为了"让容器能跑起来"直接加 --privileged,等于拆掉了容器安全模型的一整层。实际上大部分需求只需要 --cap-add=XXX 加一两个能力。
坑 3:应用报 permission denied,查了半天以为是文件权限问题
真实原因可能是容器缺 CAP_SYS_ADMIN 等能力。能力缺失和文件权限是两类完全不同的拒绝,错误信息却长得几乎一样。
坑 4:以为容器内 root 就是宿主 root
由于 Docker 默认不启用 user namespace,容器内的 uid=0 就是宿主机的 uid=0。隔离完全依赖能力位裁剪。这是容器安全模型最脆弱的一环。
二、理论:为什么需要 capabilities
传统 Unix 权限模型里,UID 0(root)是全有或全无:要么能做任何事,要么什么都做不了。
容器化需要更细的粒度——允许容器内是 root,但只给极少数特权。
Capabilities 把特权拆成 41 个独立的能力位,可单独授予、单独回收。
三个关键字段
| 字段 |
含义 |
通俗理解 |
CapPrm |
Permitted |
进程可以获得的能力上限 |
CapEff |
Effective |
进程实际正在使用的能力 |
CapBnd |
Bounding Set |
子进程能继承的最大能力集 |
CapInh |
Inheritable |
跨 execve 继承的位(默认全 0) |
CapAmb |
Ambient |
无需 file capability 即可使用的能力 |
三层安全配合
user namespace → 决定 uid 映射(Docker 默认未启用 → 容器 root = 宿主 root)
capabilities → 裁剪能做什么(默认裁掉 27 项)
seccomp → 限制能调哪些系统调用(见 05)
capabilities 管"权限位",seccomp 管"调用清单" —— 两者正交,详见 05 Seccomp。
三、实操验证
3.1 三方能力位图对比
echo "--- 宿主机 ---"
grep -E "^Cap" /proc/self/status
echo "--- 默认容器 ---"
docker run --rm alpine:3.20 sh -c 'grep -E "^Cap" /proc/self/status'
echo "--- 特权容器 ---"
docker run --rm --privileged alpine:3.20 sh -c 'grep -E "^Cap" /proc/self/status'
实测输出:
--- 宿主机 ---
CapInh: 0000000000000000
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
--- 默认容器 ---
CapInh: 0000000000000000
CapPrm: 00000000a80425fb
CapEff: 00000000a80425fb
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000
--- 特权容器 ---
CapInh: 0000000000000000
CapPrm: 000001ffffffffff
CapEff: 000001ffffffffff
CapBnd: 000001ffffffffff
CapAmb: 0000000000000000
关键差异:
宿主 CapEff: 000001ffffffffff (全 1,41 项全有)
容器 CapEff: 00000000a80425fb (仅 14 项)
注意 CapBnd 也被裁剪了——意味着容器内进程无法再获得被裁掉的能力,连 setcap 都做不到。
3.2 逐位解码
位图是 64 位十六进制,每一位对应一个能力。解码脚本:
decode_caps() {
python3 -c "
caps = {0:'CHOWN',1:'DAC_OVERRIDE',2:'DAC_READ_SEARCH',3:'FOWNER',4:'FSETID',5:'KILL',
6:'SETGID',7:'SETUID',8:'SETPCAP',9:'LINUX_IMMUTABLE',10:'NET_BIND_SERVICE',
11:'NET_BROADCAST',12:'NET_ADMIN',13:'NET_RAW',14:'IPC_LOCK',15:'IPC_OWNER',
16:'SYS_MODULE',17:'SYS_RAWIO',18:'SYS_CHROOT',19:'SYS_PTRACE',20:'SYS_PACCT',
21:'SYS_ADMIN',22:'SYS_BOOT',23:'SYS_NICE',24:'SYS_RESOURCE',25:'SYS_TIME',
26:'SYS_TTY_CONFIG',27:'MKNOD',28:'LEASE',29:'AUDIT_WRITE',30:'AUDIT_CONTROL',
31:'SETFCAP',32:'MAC_OVERRIDE',33:'MAC_ADMIN',34:'SYSLOG',35:'WAKE_ALARM',
36:'BLOCK_SUSPEND',37:'AUDIT_READ',38:'PERFMON',39:'BPF',40:'CHECKPOINT_RESTORE'}
v = int('$1', 16)
have = [n for b,n in caps.items() if v >> b & 1]
miss = [n for b,n in caps.items() if not (v >> b & 1)]
print(' 拥有 (' + str(len(have)) + '): ' + ', '.join(have))
print(' 缺失 (' + str(len(miss)) + '): ' + ', '.join(miss))
"
}
宿主机:
拥有 (41): CHOWN, DAC_OVERRIDE, DAC_READ_SEARCH, FOWNER, FSETID, KILL, SETGID,
SETUID, SETPCAP, LINUX_IMMUTABLE, NET_BIND_SERVICE, NET_BROADCAST, NET_ADMIN,
NET_RAW, IPC_LOCK, IPC_OWNER, SYS_MODULE, SYS_RAWIO, SYS_CHROOT, SYS_PTRACE,
SYS_PACCT, SYS_ADMIN, SYS_BOOT, SYS_NICE, SYS_RESOURCE, SYS_TIME, SYS_TTY_CONFIG,
MKNOD, LEASE, AUDIT_WRITE, AUDIT_CONTROL, SETFCAP, MAC_OVERRIDE, MAC_ADMIN,
SYSLOG, WAKE_ALARM, BLOCK_SUSPEND, AUDIT_READ, PERFMON, BPF, CHECKPOINT_RESTORE
缺失 (0):
默认容器:
拥有 (14): CHOWN, DAC_OVERRIDE, FOWNER, FSETID, KILL, SETGID, SETUID, SETPCAP,
NET_BIND_SERVICE, NET_RAW, SYS_CHROOT, MKNOD, AUDIT_WRITE, SETFCAP
缺失 (27): DAC_READ_SEARCH, LINUX_IMMUTABLE, NET_BROADCAST, NET_ADMIN, IPC_LOCK,
IPC_OWNER, SYS_MODULE, SYS_RAWIO, SYS_PTRACE, SYS_PACCT, SYS_ADMIN, SYS_BOOT,
SYS_NICE, SYS_RESOURCE, SYS_TIME, SYS_TTY_CONFIG, LEASE, AUDIT_CONTROL,
MAC_OVERRIDE, MAC_ADMIN, SYSLOG, WAKE_ALARM, BLOCK_SUSPEND, AUDIT_READ,
PERFMON, BPF, CHECKPOINT_RESTORE
被裁掉的 27 项里,最危险的是:SYS_ADMIN、SYS_MODULE、SYS_PTRACE、NET_ADMIN、BPF、PERFMON、MAC_ADMIN、AUDIT_CONTROL。
3.3 能力缺失的实际影响
mount 需要 CAP_SYS_ADMIN:
docker run --rm alpine:3.20 sh -c 'mkdir -p /tmp/x && mount -t tmpfs none /tmp/x 2>&1'
mount: permission denied (are you root?)
容器内明明是 root(uid=0),却 mount 不了文件系统。 因为 CAP_SYS_ADMIN 被裁掉了。
3.4 精确加回单项能力
docker run --rm --cap-add=SYS_ADMIN alpine:3.20 sh -c \
'mkdir -p /tmp/x && mount -t tmpfs none /tmp/x 2>&1 && echo "mount 成功"'
mount 成功
只需要一项能力就能达成目的,不需要 --privileged。
3.5 主动裁掉能力
docker run --rm --cap-drop=CHOWN alpine:3.20 sh -c 'grep CapEff /proc/self/status'
CapEff: 00000000a80425fa
对比默认的 a80425fb——末位从 b(1011)变成 a(1010),最低位被清掉了,正是 CAP_CHOWN(bit 0)。
3.6 特权容器
docker run --rm --privileged alpine:3.20 sh -c \
'mkdir -p /tmp/x && mount -t tmpfs none /tmp/x && echo "mount 成功"'
mount 成功
--privileged 恢复全部 41 项能力,等于把 capability 这一层防御完全拆除。
3.7 能力裁剪 vs seccomp 的边界
这两层容易被混淆,用一组对照实验精确区分:
| 场景 |
mount 结果 |
| 默认(无 SYS_ADMIN,seccomp 开启) |
失败,退出码 1 |
--cap-add=SYS_ADMIN(seccomp 开启) |
成功,退出码 0 |
--cap-add=SYS_ADMIN --security-opt seccomp=unconfined |
成功,退出码 0 |
--security-opt seccomp=unconfined(无 SYS_ADMIN) |
失败,退出码 1 |
结论:真正拦截 mount 的是 CAP_SYS_ADMIN 缺失,不是 seccomp。
Docker 默认 seccomp profile 对 mount 是有条件放行的——有 CAP_SYS_ADMIN 时允许,没有时由能力检查拦截。
所以遇到 "Operation not permitted" 时,先查能力位,再查 seccomp:
docker exec <c> grep CapEff /proc/self/status
docker exec <c> grep Seccomp /proc/self/status
四、能力清单与用途
容器默认保留的 14 项
| 能力 |
用途 |
为什么需要 |
CAP_CHOWN |
改变文件属主 |
包管理、文件操作 |
CAP_DAC_OVERRIDE |
绕过文件读权限检查 |
访问属主不属于自己的文件 |
CAP_FOWNER |
跳过属主检查 |
chmod 等 |
CAP_FSETID |
设置文件属主 ID |
归档文件解压 |
CAP_KILL |
发送信号给任意进程 |
应用内进程管理 |
CAP_SETGID / CAP_SETUID |
改组/用户 ID |
降权运行 |
CAP_SETPCAP |
修改能力集 |
应用自行调整权限 |
CAP_NET_BIND_SERVICE |
绑定 1024 以下端口 |
Web 服务监听 80 |
CAP_NET_RAW |
原始套接字 |
ping、traceroute |
CAP_SYS_CHROOT |
chroot |
应用内部切换根目录 |
CAP_MKNOD |
创建设备文件 |
少数场景需要 |
CAP_AUDIT_WRITE |
写审计日志 |
部分审计框架 |
CAP_SETFCAP |
设置文件能力 |
少数应用 |
被裁掉的高危能力
| 能力 |
用途 |
裁掉后的影响 |
CAP_SYS_ADMIN |
mount、内核参数 |
最常见的坑,容器不能 mount |
CAP_SYS_MODULE |
加载内核模块 |
无法加载驱动 |
CAP_SYS_PTRACE |
调试其他进程 |
无法 attach 到别的进程 |
CAP_NET_ADMIN |
网络配置 |
无法改路由/网卡 |
CAP_BPF |
eBPF |
无法用 bpf 工具 |
CAP_PERFMON |
性能监控 |
perf 不可用 |
CAP_MAC_ADMIN |
SELinux 管理 |
无法改标签 |
CAP_AUDIT_CONTROL |
审计控制 |
无法操作审计子系统 |
CAP_DAC_READ_SEARCH |
绕过读权限 + 搜索 |
读取更受限 |
CAP_SYS_BOOT |
重启系统 |
不能 reboot |
常见需求与对应能力
| 需求 |
需要的能力 |
| 应用要监听 80 端口 |
NET_BIND_SERVICE(默认已有) |
| 应用要 mount 卷 |
SYS_ADMIN(强烈不建议) |
| 应用要改网络配置 |
NET_ADMIN |
| 应用要发 ICMP |
NET_RAW(默认已有) |
| 应用要 ptrace 调试自己 |
SYS_PTRACE |
| 应用要读别的用户的文件 |
DAC_READ_SEARCH |
原则:能用 --cap-add 解决就绝不用 --privileged;能用具体能力就不加整个集合。
五、安全边界
容器共享内核——最大的风险面
docker run --rm alpine:3.20 uname -r
# 5.14.0-611.55.1.el9_7.x86_64 ← 和宿主机完全相同
容器和宿主机跑同一个内核。如果内核有容器逃逸漏洞(dirty cow 类),namespace 隔离可能被击穿。
隔离强度谱系
容器共享内核 ← Docker 默认,隔离较弱
─────────────────────────────
gVisor / Kata 用户态内核,拦截系统调用
─────────────────────────────
轻量 VM (Firecracker) 独立微型内核
─────────────────────────────
完整 VM 独立内核 + Hypervisor
高敏感多租户场景应选 gVisor 或 VM,而不是裸容器。
Docker 的安全加固清单
| 机制 |
说明 |
默认状态 |
| AppArmor / SELinux |
强制访问控制标签 |
RHEL 系默认启用 SELinux |
| seccomp |
系统调用白名单 |
默认启用(见 05) |
| capabilities 裁剪 |
移除危险权限位 |
默认启用(14/41) |
| user namespace |
UID 映射,root 非真 root |
默认未启用,需 --userns-remap |
| 只读根文件系统 |
--read-only,防篡改 |
需手动加 |
| no-new-privileges |
禁止 setuid 提权 |
需手动加 |
| 网络隔离 |
自定义 network + 不暴露端口 |
需手动配 |
--privileged 会同时关闭以上多项,这是生产环境最需要避免的操作。
宿主 root 的绝对权力
nsenter -t $(docker inspect <c> --format '{{.State.Pid}}') -m -p --preserve-credentials
宿主机 root 可以随时进入容器的所有 namespace。这是设计使然(运维需要),也意味着:宿主机被攻破 = 所有容器沦陷。
六、常见误解
误解 1:容器内是 root 所以什么都能干
错。 实测容器内 UID=0,但 mount 被拒绝,因为 CAP_SYS_ADMIN 被裁掉了。
容器 CapEff: 00000000a80425fb ← 14 项能力
宿主 CapEff: 000001ffffffffff ← 41 项能力
误解 2:容器内 root = 宿主 root
从 UID 角度讲,是的;从权限角度讲,不是。
因为 Docker 默认不启用 user namespace(uid_map 宿主容器完全一致:0 0 4294967295),容器内 root 的 UID 确实是 0。但能力位被裁剪,所以实际能做的事远少于宿主 root。
准确说法:容器内 root 是"能力受限的 root",不是"权力完整但被 namespace 隔离的 root"。要后者需要启用 user namespace。
误解 3:需要更多权限就该用 --privileged
错,应该用 --cap-add。
# 不好
docker run --privileged ...
# 好
docker run --cap-add=SYS_ADMIN ...
docker run --cap-add=NET_ADMIN --cap-add=SYS_PTRACE ...
--privileged 关闭了 capability 裁剪、seccomp、SELinux 标签等多层防护,通常只需要其中一两个能力。
误解 4:能力缺失和文件权限不足是一回事
不是。 两者错误信息都是 "Permission denied",但根因和修法完全不同:
| 类型 |
检查方法 |
修法 |
| 文件权限 |
ls -l 看 owner/group/mode |
chown / chmod |
| 能力缺失 |
grep CapEff /proc/self/status |
--cap-add=XXX |
排查顺序:先 id 确认 uid,再 ls -l 看文件属主,再查能力位。
误解 5:容器比虚拟机更安全
不一定。 容器共享内核,隔离强度弱于虚拟机。
准确说法:容器在运维便利性和资源效率上有优势,在隔离强度上弱于 VM。安全与否取决于威胁模型和加固配置。
误解 6:CapBnd 被裁剪后还能加回来
不能。 实测默认容器的 CapBnd 也是 a80425fb,说明被裁掉的能力连尝试恢复的资格都没有。
只有 --privileged 或显式 --cap-add 能在创建时恢复。
七、本篇速查
# 看能力位图
grep -E "^Cap" /proc/self/status
docker exec <c> grep -E "^Cap" /proc/self/status
# 解码(见 3.2 的 decode_caps 函数)
# 常用能力加回
docker run --cap-add=SYS_ADMIN ...
docker run --cap-add=NET_ADMIN --cap-add=SYS_PTRACE ...
docker run --cap-drop=CHOWN ...
# 查看容器实际生效的配置
docker inspect <c> | python3 -c "
import json,sys
d=json.load(sys.stdin)[0]['HostConfig']
print('Privileged:', d.get('Privileged'))
print('CapAdd:', d.get('CapAdd'))
print('CapDrop:', d.get('CapDrop'))
print('SecurityOpt:', d.get('SecurityOpt'))
"
# 排查 permission denied 的三步
docker exec <c> id # 1. uid 是什么
docker exec <c> ls -l /path/to/file # 2. 文件属主权限
docker exec <c> grep CapEff /proc/self/status # 3. 能力够不够
---
> 上一篇:[03 · Cgroup:资源限额](03-Cgroup-资源限额.md)
>
> 下一篇:[05 · Seccomp:系统调用过滤](05-Seccomp-系统调用过滤.md)
>
> 相关:[专题索引](README.md)