Linux Capabilities 提权防护实战配置

适用场景

Linux Capabilities 提权防护是主机加固中容易被跳过的一环。很多管理员清理了 SUID 文件、收紧了 sudo 配置,却忽略了文件 capabilities 这条等价的特权通道。setcap 写入的权限存储在文件的扩展属性中,常规的”查找 SUID”扫描命令完全查不到它

以下场景需要做 capabilities 审计与收敛:

  • 服务器已完成 SUID 清理,但合规基线仍要求覆盖全部特权通道
  • 运行需要绑定低端口、原始套接字、挂载能力的自研服务
  • 使用 Docker / containerd 部署容器,需校验容器实际持有的 capability 集
  • 应急响应中排查攻击者植入的持久化后门(cap_setuid + cap_setgid 可让普通进程任意切换 UID)

本文给出盘点、收敛、迁移到 systemd 托管、容器加固、变更监控五个步骤的完整可复制方案。

前置条件

  • Linux 内核 2.6.24 及以上(capabilities 正式可用),CONFIG_SECURITY_FILE_CAPABILITIES 已开启
  • 已安装 libcap 工具集:getcapsetcapcapsh(CentOS 对应包名 libcap,Debian/Ubuntu 为 libcap2-bin
  • 具备 root 权限或 CAP_SETFCAP
  • 文件系统支持扩展属性,且挂载时未使用 nouser_xattr 之类会剥离 xattr 的选项
  • 测试环境可用于验证服务在移除 capability 后的行为
# 安装工具(按发行版择一)
yum install -y libcap attr                 # RHEL / CentOS / 麒麟
apt-get install -y libcap2-bin attr        # Debian / Ubuntu

原理说明

传统 Unix 权限模型是二元的:进程要么是 root(全部权限),要么是普通用户(几乎没有特权)。SUID 位把可执行文件临时提权到文件属主,粒度极粗且难以审计。Capabilities 把 root 的权限拆成约 40 个独立的特权位,进程可以只持有其中若干项。

关键概念:

  • File capabilities:存储在文件的 security.capability 扩展属性里,由 setcap 写入。任何用户执行该文件时,进程会获得相应特权位。这就是”替代 SUID 的提权通道”。
  • Process capabilities:进程当前持有的集合,可通过 /proc/<pid>/statusCapEff 读取,十六进制位掩码格式。
  • Bounding set:进程能获取的特权上限。普通用户的 bounding set 通常是全集,因此文件 capability 可以生效;通过 capsh --drop 或 systemd 的 CapabilityBoundingSet 可以缩小它,这是防提权的关键手段。

需要重点关注的高危 capability

  • cap_sys_admin:几乎等于 root,可挂载文件系统、操作内核模块参数。授予其任意可执行文件都是严重配置错误。
  • cap_setuid / cap_setgid:任意切换进程身份,等价于无条件提权。
  • cap_dac_read_search:绕过文件的读权限与目录搜索权限检查,可读取 /etc/shadow
  • cap_sys_ptrace:注入并操纵其他进程内存,可窃取凭据。
  • cap_net_raw:原始套接字,可抓包与构造任意报文。
  • cap_sys_module:加载内核模块,直接内核态执行。

还需要注意一个常见误区:文件 capability 不随文件复制而可靠保留。用 cp -arsync -X 可以带过去,用普通 cptar(不带 --xattrs)或从 ZIP 包解压则不会。这既是安全特性(解压漏洞无法直接带来特权文件),也导致恢复备份后服务异常。

操作步骤

1. 全盘盘点现有文件 capabilities

# 递归扫描全盘,忽略 proc/sys 等伪文件系统
getcap -r / 2>/dev/null

# 只输出到文件便于比对(作为基线快照)
getcap -r / 2>/dev/null | sort > /root/cap-baseline-$(date +%F).txt
wc -l /root/cap-baseline-$(date +%F).txt

典型输出形如 /usr/bin/ping cap_net_raw=ep,其中 =ep 表示 effective 与 permitted 位同时置位,即”执行时立即生效”。

2. 逐项判定并移除不必要的能力

原则是能移除就移除。系统自带的 pingtraceroute 若业务确实需要普通用户使用,可保留 cap_net_raw;若服务器为纯 Web 角色、普通用户不需要 ping,可直接清除:

# 移除单个文件的全部 capability
setcap -r /usr/bin/ping

# 只保留必要的子集(示例:仅保留 net_bind_service)
setcap 'cap_net_bind_service=+ep' /usr/local/bin/myservice

# 校验结果
getcap /usr/local/bin/myservice

# 批量清除高危授权,输出被处理清单
getcap -r / 2>/dev/null | grep -Ei 'cap_sys_admin|cap_setuid|cap_setgid|cap_sys_ptrace|cap_sys_module' \
  | awk '{print $1}' | tee /root/cap-risky.txt \
  | while read -r f; do setcap -r "$f"; done

处理前建议先确认 /root/cap-risky.txt 中的路径是否属于第三方安全产品(部分 EDR、监控代理会使用 capability),误删会导致其静默失效。

3. 用 systemd 托管替代文件 capability

直接给二进制文件打 capability 的问题是:任何用户执行该文件都会获得特权,且升级覆盖后配置丢失。推荐改为让 systemd 在启动服务时授予,并同时收窄 bounding set:

# /etc/systemd/system/myservice.service
[Unit]
Description=My privileged service
After=network.target

[Service]
Type=simple
User=svcuser
Group=svcuser
ExecStart=/usr/local/bin/myservice

# 仅授予绑定低端口所需的最小能力
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
# 禁止进程再获得任何新特权(防提权的核心开关)
NoNewPrivileges=yes
# 配合文件系统与命名空间隔离
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelModules=yes
ProtectKernelTunables=yes
RestrictNamespaces=yes
ReadWritePaths=/var/lib/myservice

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl restart myservice
# 读回实际生效的能力集
systemctl show myservice -p CapabilityBoundingSet -p AmbientCapabilities -p NoNewPrivileges
cat /proc/$(systemctl show -p MainPID --value myservice)/status | grep -E 'CapEff|CapBnd|NoNewPrivs'

NoNewPrivileges=yes 会在内核层面阻止 execve 时获得任何新特权,等价于设置 prctl(PR_SET_NO_NEW_PRIVS)。开启后,即使进程被诱导执行了带 SUID 位或带文件 capability 的二进制,也无法提权。这一项对防提权价值最高,且对绝大多数服务无副作用。

4. 容器侧的 capability 收敛

Docker 默认就给容器约 14 项 capability,其中包含 cap_net_rawcap_chowncap_setuid 等。生产容器应改为白名单模式:

# 丢弃全部,只加回真正需要的
docker run -d --name web \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --security-opt=no-new-privileges:true \
  --user 1000:1000 \
  nginx:stable

# 校验实际能力集,应为空或仅含 net_bind_service
docker inspect web --format '{{json .HostConfig.CapDrop}} {{json .HostConfig.CapAdd}}'
docker exec web grep CapEff /proc/1/status

使用 Compose 或 Kubernetes 时同样配置,Kubernetes 中对应:

securityContext:
  runAsNonRoot: true
  runAsUser: 1000
  allowPrivilegeEscalation: false        # 对应 no-new-privileges
  readOnlyRootFilesystem: true
  capabilities:
    drop: ["ALL"]
    add: ["NET_BIND_SERVICE"]

5. 建立变更监控

攻击者若通过文件 capability 做持久化,落地动作必然调用 setcap 或直接写 xattr。用 auditd 同时覆盖两个入口:

# /etc/audit/rules.d/cap.rules
# 监控 setcap/setfcap 二进制执行
-a always,exit -F path=/usr/sbin/setcap -F perm=x -F auid>=1000 -F auid!=4294967295 -k cap_change
-a always,exit -F path=/usr/sbin/setfcap -F perm=x -F auid>=1000 -F auid!=4294967295 -k cap_change
# 监控对扩展属性的写操作(绕过命令直接写 xattr 的情况)
-a always,exit -F arch=b64 -S setxattr -S lsetxattr -S fsetxattr -F auid>=1000 -F auid!=4294967295 -k xattr_change
# 监控可疑的高危 capability 文件名变更
-w /usr/local/bin/ -p wa -k bin_change
augenrules --load
auditctl -l | grep -E 'cap_change|xattr_change'

# 定期基线比对(建议加入 crontab 每日执行)
getcap -r / 2>/dev/null | sort > /var/log/cap-now.txt
diff /root/cap-baseline-*.txt /var/log/cap-now.txt && echo "OK: no capability change" \
  || logger -t capwatch "WARN: file capabilities changed"

配置验证

1. 验证特权确实被移除

# 在普通用户下执行,期望 Permission denied
sudo -u nobody /usr/bin/ping -c1 127.0.0.1 ; echo "exit=$?"

# 确认该文件不再带 capability
getcap /usr/bin/ping   # 期望无输出

2. 验证 NoNewPrivileges 生效

# 以一个带 SUID 的文件做测试目标,普通用户在服务沙箱内不应获得 root
systemctl show myservice -p NoNewPrivileges   # 期望 yes
grep NoNewPrivs /proc/$(systemctl show -p MainPID --value myservice)/status

3. 验证容器能力集

# 输入为 8 位十六进制,全 0 表示无任何特权
docker exec web grep CapEff /proc/1/status
# 可读化解析
docker exec web capsh --decode=$(docker exec web grep CapEff /proc/1/status | awk '{print $2}')

验证通过的标准:cap_chowncap_setuidcap_setgidcap_net_rawcap_sys_admin 均不出现在解码结果中。

4. 验证审计规则触发

setcap 'cap_net_bind_service=+ep' /tmp/testcap ; rm -f /tmp/testcap
ausearch -k cap_change -ts recent | tail -20   # 期望看到上述动作记录

常见问题

FAQ 1:移除了 cap_net_raw 之后普通用户无法使用 ping,怎么兼顾安全与运维?

三个方案,按推荐度排序:一是仅对运维组放行,使用 setcap 'cap_net_raw=+ep' /usr/bin/ping 保留文件能力,同时确认 /usr/bin/ping 属主为 root、且普通用户组外不可写,风险可控;二是改用无特权探测工具,如 curltcping 做端口连通性检测,绕开 ICMP 需求;三是在堡垒机侧统一提供探测能力,生产节点不放行任何普通用户的网络探测。若服务器为纯 Web 角色且无运维交互,直接移除是更彻底的选择。

FAQ 2:清除了某个服务的 capability 后服务启动失败,如何快速回滚?

先确认错误类型。bind() failed: Permission denied 说明缺少 cap_net_bind_service,此时不要立刻把文件 capability 加回去,正确做法是在 systemd unit 中通过 AmbientCapabilities=CAP_NET_BIND_SERVICE 授予,作用域限定在该服务而非全局。若服务以非 root 用户运行且需要绑定 80 端口,还可用 sysctl -w net.ipv4.ip_unprivileged_port_start=80 放开低端口限制,这比授予 capability 更细粒度。回滚期间可临时执行 setcap 'cap_net_bind_service=+ep' /path/to/bin 恢复服务,但必须记录并在后续替换为 systemd 方案。

FAQ 3:容器里丢弃了 capability,为什么从容器内看 CapEff 仍非全 0?

Docker 的 --cap-drop=ALL 清空的是”额外授予集”,但容器内 PID 1 仍可能因 --privileged、root 用户默认集或运行时保留项而持有部分能力。排查顺序:先确认启动命令确实包含 --cap-drop=ALLdocker inspectHostConfig.CapDrop 字段),再确认未使用 --privileged;随后检查是否以 root 运行(CapEff 会继承),改用 --user 1000:1000;最后核对是否由 Compose/K8s 覆盖了配置。需要强调的是:只要容器内进程是 root 且允许 privilege escalation,丢弃 capability 的收益会大幅缩水,因此 allowPrivilegeEscalation: falserunAsNonRoot: true 必须同时开启。

总结

Linux Capabilities 是 SUID 之外的独立特权通道,配置不当会成为提权与权限维持的入口。落地要点:

  • 盘点getcap -r / 建立基线,任何新增都可被 diff 发现。
  • 收敛:清除 cap_sys_admincap_setuidcap_sys_ptrace 等高危授予,只保留业务必需项。
  • 迁移:从”给文件打 capability”改为 systemd AmbientCapabilities + NoNewPrivileges=yes,作用域更小、可审计。
  • 容器--cap-drop=ALL 配合非 root 运行与 allowPrivilegeEscalation: false
  • 监控:auditd 覆盖 setcap 执行与 xattr 写入两条路径。

其中 NoNewPrivileges 的投入产出比最高:一行配置即可阻断整条”借 SUID / capability 提权”的链条。