为什么每个教程都让你用 root,而这恰恰是问题所在
装 Nginx 要 root,绑 80 端口要 root,改配置要 root,重启服务要 root。久而久之,我们在服务器上养成了一个非常坏的习惯:sudo su - 然后一整天都待在 root shell 里。所有进程都以 root 运行,所有文件都是 root 所有,任何一次配置错误、任何一个被利用的漏洞,都直接等于"整个服务器沦陷"。
其实 Linux 早就给了一个更细粒度的答案:Capabilities(能力)。它把传统 root 那一个无敌的权限大礼包,拆成了三十多个独立的小权限。你可以让一个普通用户或普通进程只拿到"监听 80 端口"这一项能力,而完全不给它"读写任意文件""改内核参数""挂载文件系统"的能力。这篇文章就是讲怎么把这项能力真正用起来:怎么查、怎么给、怎么在 systemd 和容器里配,以及最容易踩的坑。
Capabilities 到底拆了什么
传统 Unix 权限模型很粗暴:UID 0 就是神,非 0 就是凡人。但现实里"以 root 运行 Nginx"其实只需要其中很小一部分权限,比如绑定低端口、读取某些受保护文件。Capabilities 就是把 root 的特权拆开,常见的几项:
| 能力 | 作用 | 典型场景 |
|---|---|---|
| CAP_NET_BIND_SERVICE | 绑定 1024 以下的特权端口 | 非 root 跑 Nginx/HTTP 服务绑 80/443 |
| CAP_NET_RAW | 使用原始套接字 | ping、tcpdump |
| CAP_NET_ADMIN | 配置网络接口、路由、防火墙 | VPN、容器网络配置 |
| CAP_SYS_ADMIN | 挂载、命名空间、大量系统管理操作 | "新的 root",能不给就不给 |
| CAP_SYS_TIME | 设置系统时钟 | NTP 客户端 |
| CAP_CHOWN | 修改文件属主 | 需要移交文件所有权的服务 |
| CAP_DAC_OVERRIDE | 绕过文件读写权限检查 | 几乎等价于全盘可读写,慎用 |
| CAP_SETUID / CAP_SETGID | 切换进程身份 | 需要运行时降权的服务 |
其中 CAP_SYS_ADMIN 和 CAP_DAC_OVERRIDE 是两个"送分题变送命题"的能力,它们基本等于把 root 还回去了。真正的最小权限实践,核心就是:能给 CAP_NET_BIND_SERVICE 这种单点能力,就绝不给 SYS_ADMIN。
第一步:先看清楚一个进程到底拿着哪些能力
排查权限问题最常用的两个命令是 getpcaps(按 PID 查)和 getcap(按文件查二进制上的 capability 位)。
# 按 PID 查看进程当前拥有的能力(需要先安装 libcap2-bin / libcap-ng-utils)
apt-get install -y libcap2-bin
# 查当前 shell 的能力(普通用户通常什么都没有)
getpcaps $$
# 查 nginx 主进程(假设 PID 1234)
getpcaps 1234
# 更直观的方法:直接读 /proc
cat /proc/1234/status | grep -i cap
# CapInh: 0000000000000000 # 继承能力
# CapPrm: 0000000000000400 # 允许能力
# CapEff: 0000000000000400 # 有效能力 ← 实际生效的就是这个
# CapBnd: 000001ffffffffff # 边界集合,限制了能拿到能力的上限
CapEff 是十六进制位掩码,光看数字没意义,要用 capsh --decode 翻译:
capsh --decode=0000000000000400
# 输出: 0x0000000000000400=cap_net_bind_service
再看文件层面。很多发行版早就悄悄给一些常用二进制打了补丁,装完 ping 你就能以普通用户运行,原因就是:
getcap -r / 2>/dev/null
# 典型输出:
# /usr/bin/ping cap_net_raw=ep
# /usr/bin/traceroute cap_net_raw+ep
# /usr/bin/newuidmap cap_setuid=ep
#
# 这里的 =ep 表示 e(ffective) 和 p(ermitted) 都置位
记住 getcap -r / 这个命令,安全审计时它非常有用:任何被设置了 capability 的二进制,都可能是一个提权入口,尤其是那些可被普通用户写入的路径。
第二步:让非 root 进程绑定 80 端口
这是 capabilities 最经典、最实用的场景。传统做法有两种:要么整个 Nginx 用 root 起(ngnix 的 worker 会降权,但 master 仍是 root),要么用 authbind、setcap 或者反代到一个高端口。最干净的做法是给 Nginx 二进制设置能力。
# 方案 A:给二进制文件设置 capability,之后任何用户运行它都能绑低端口
# ⚠️ 注意:这是全局的,且 Nginx 升级后二进制被替换,能力会丢失,需要重设
setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx
# 验证
getcap /usr/sbin/nginx
# /usr/sbin/nginx cap_net_bind_service=ep
# 之后把 nginx.conf 里的 user 指令设成非 root 用户
# user www-data;
# 方案 B(更推荐):不用 setcap,改用 systemd 的 AmbientCapabilities
# 见下一节
方案 A 有个明显缺点:它是绑定在文件上的,包管理器一升级就把设置冲掉了,而且一旦二进制可被非特权用户替换,就等于给了对方一个提权跳板。所以生产环境更推荐用 systemd 来做,因为它把能力绑定在 unit 上,跟文件权限解耦。
systemd 里的正确写法
# /etc/systemd/system/nginx.service.d/caps.conf
[Service]
# 把自己降权到 www-data 运行
User=www-data
Group=www-data
# 只抛出绑定特权端口这一项能力
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# 顺手把其他攻击面关掉
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
# 需要写入的目录单独放开
ReadWritePaths=/var/log/nginx /var/lib/nginx /run
# 重新加载并验证
systemctl daemon-reload
systemctl restart nginx
systemctl show nginx -p AmbientCapabilities -p CapabilityBoundingSet
# 确认进程不再是 root
ps -o pid,user,comm -C nginx
getpcaps $(pgrep -f 'nginx: master')
这段配置里三个参数的关系要理清:CapabilityBoundingSet 决定"这个服务最多能拿到哪些能力"(是天花板),AmbientCapabilities 决定"实际交到进程手上的能力"(是实际值)。只设 Ambient 不设 Bounding 是不够严谨的,BoundingSet 才是真正把攻击面钉死的那个。
第三步:容器场景下的能力裁剪
Docker 默认给容器保留了一批能力,其中一些比你想的要多。真正的生产容器应该做"白名单"而不是"黑名单"。
# 先看某个运行中容器实际保留了哪些能力
docker inspect --format '{{.HostConfig.CapAdd}} / {{.HostConfig.CapDrop}}' myapp
# 在容器内看有效能力(需要容器里有 capsh)
docker exec -it myapp capsh --print
# 推荐的启动方式:drop ALL,再按需 add
docker run -d \
--name web \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
-p 80:8080 \
nginx:alpine
--cap-drop=ALL 之后再 --cap-add=NET_BIND_SERVICE,这是容器安全的基本功。很多镜像的健康检查会用到 ping,那就单独加 --cap-add=NET_RAW;如果没有这个需求,千万别为了省事把 SYS_ADMIN 加回去。Compose 里对应写法是:
services:
web:
image: nginx:alpine
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
顺带说一句,no-new-privileges:true 这条非常值得加。它禁止进程通过 setuid 程序或文件 capability 获得新的特权,等于把"容器里藏了个 setuid 二进制当提权跳板"这条路直接堵死。
踩坑清单:这些坑我都真实踩过
坑 1:setcap 之后程序反而起不来了
给一个非 ELF 或者带脚本头的程序 setcap,会得到 Operation not supported。Capabilities 只能应用于二进制可执行文件,不能应用于 shell 脚本。如果你给一个脚本 setcap 然后用 shebang 执行,内核会拒绝设置。解决办法是用一个编译型的小包装器,或者改用 systemd 的 AmbientCapabilities。
坑 2:升级软件后权限莫名其妙消失了
apt/yum 升级会替换二进制文件,新的文件没有 capability 位。所以用 setcap 的方案必须配合"升级后重新 setcap"。这也是我更推荐 systemd AmbientCapabilities 的原因——它跟文件解耦,升级多少次都还在。
坑 3:AmbientCapabilities 设置了但没生效
常见原因有三个:一是同时设了 NoNewPrivileges=true 却忘了 Ambient(这俩不冲突,但 Ambient 需要 bounding set 允许);二是 CapabilityBoundingSet 里没有包含你 Ambient 想给的能力,天花板比实际值小,结果是空的;三是服务启动时用的是 User= 指定用户,但该用户切换发生在能力丢失之后。排查顺序:先 systemctl show 看配置是否读到,再在服务里跑 cat /proc/self/status | grep Cap 看实际值。
坑 4:以为 drop 了能力就安全了
能力只是其中一层。很多真实提权是走内核漏洞、SUID 二进制、可写配置文件这些路径,跟 capabilities 无关。capabilities 的价值是"减小区间",不是"免死金牌"。配合 ProtectSystem、ReadWritePaths、AppArmor/SELinux 一起用才完整。
坑 5:把 CAP_DAC_OVERRIDE 当成方便工具
有人为了省去配文件权限的麻烦,直接给服务加 CAP_DAC_OVERRIDE,等于让进程绕过所有文件权限检查。这不是最小权限,这是换了个名字的 root。遇到权限问题应该去修文件属主和权限位,而不是给能力。
排查能力问题的一套固定动作
# 1. 看服务配置里到底设了什么
systemctl show mysvc -p User -p Group -p AmbientCapabilities -p CapabilityBoundingSet
# 2. 看进程实际拿到了什么
PID=$(systemctl show -p MainPID --value mysvc)
getpcaps "$PID"
grep -i cap /proc/"$PID"/status
# 3. 看二进制上有没有遗留的 setcap
getcap /usr/sbin/mysvc
# 4. 看内核审计日志(如果开了 audit)
ausearch -m capability --start today | tail -20
常见问题(FAQ)
Q1:给 Nginx 加了 NET_BIND_SERVICE 之后,还需要 root master 进程吗?
不需要。Nginx 的 master 可以用非 root 身份启动,worker 也是同一个非特权用户。但要注意 user 指令在非 root 启动时是无效的(会报 warning),因为进程本来就没有切换身份的能力(没有 CAP_SETUID)。这正好符合我们的预期:不给就不需要切。
Q2:为什么我给文件 setcap 后,普通用户执行仍然失败?
因为文件本身的可执行权限位没给到位,或者所在路径对用户不可执行,或者文件系统挂了 nosuid/noexec。另外别忘了检查 SELinux/AppArmor 是否拦了。先从 ls -l 和 mount | grep $(df --output=target file | tail -1) 入手。
Q3:容器里用 root 跑真的很危险吗?
取决于映射方式。默认的 rootful 容器里 root 就是宿主机的 root(在 user namespace 未启用的情况下),一旦逃逸或挂载了宿主目录,代价很大。至少应该做:drop ALL 能力、no-new-privileges、只读挂载、必要时用 rootless 模式。如果达不到,就用 --user 1000:1000 以非 root 运行。
Q4:capabilities 和 sudo 有什么区别?
sudo 是"临时把整个人提权到 root 去执行一条命令",能力粒度大、审计依赖 sudo 日志;capabilities 是"让进程永久只持有某一项小能力"。前者适合人工运维操作,后者适合长期运行的服务进程。给服务用 sudo 是反模式。
Q5:怎么快速审计一台机器上所有带 capability 的文件?
getcap -r / 2>/dev/null | sort
再配合"该文件是否可被非特权用户写入"的判断,就基本能找出一批潜在提权点。特别留意 /tmp、/home、挂载的共享目录里出现带 capability 的二进制,那几乎肯定是异常的。
写在最后
最小权限不是一个开关,而是一整套习惯。capabilities 只是其中最容易见效的一环:把服务从 root 降下来、把不需要的能力 drop 掉、把边界集合钉死。做完这几步,你就从"一个漏洞等于全盘沦陷"变成了"一个漏洞只影响一个服务"。这件事的投入产出比极高,值得每个自己管服务器的人花一个下午认真做一遍。下次再看到教程开头那句 sudo su -,你可以多想一秒:这里真的需要 root 吗,还是只需要一项 capability?