本节摘要:Linux capabilities 把 root 的全能拆成几十个独立的能力位,容器安全的日常操作就是对这些位做加减法。本节审计两种引擎的默认能力集合,讲常见需求(绑定低端口、抓包、改系统时间)的最小开位方案,以及"能减则减"的实践——用 drop 清单把容器降到刚好够用的权限水位。
传统 Unix 的权限模型里 root 是开关式的:要么全能要么凡人。现实里 root 的"全能"其实是几十件具体的事——绑定特权端口(CAP_NET_BIND_SERVICE)、改文件属主(CAP_CHOWN)、抓别人的包(CAP_NET_RAW)、加载内核模块(CAP_SYS_MODULE)、改系统时钟(CAP_SYS_TIME)……capabilities 机制把这些事拆成了独立的位。容器技术的核心安全操作由此变成:默认给容器一小把钥匙,需要时精确补发,不用的坚决收回。
两个引擎在这件事上共享同一套内核机制与几乎相同的默认集合(都源自 OCI 的默认 capability 清单,十几位:CHOWN、DAC_OVERRIDE、FOWNER、SETUID、NET_BIND_SERVICE、KILL……)。真正的分野在上一章讲过的"身份层":Docker 里容器默认以 root 身份持有这把钥匙串,Podman rootless 里容器内的 root 是映射身份,某些位(比如影响宿主全局的 SYS_TIME)拿了也只是容器剧场里的道具。同样的默认清单,落到不同身份模型上,实际威力不同——这是审计两引擎时要先建立的坐标系。
开位原则:一次只加一个位,加完能跑就收手。三个高频案例:
# 需求一:应用要监听 80 端口(rootless 下 1024 以下默认被拒) podman run --rm --cap-add NET_BIND_SERVICE \ nginx:alpine nginx -g 'daemon off;' # 该位授予"绑定低端口"这一件事,不给任何其他特权。 # 注意:rootless 下部分内核要求此位经由用户命名空间映射, # 具体取决于发行版的 sysctl 配置——失败了再考虑宿主侧端口转发 # 需求二:网络诊断工具需要 raw socket podman run --rm --cap-add NET_RAW --cap-add NET_ADMIN \ netshoot:latest tcpdump -i eth0 -c 5 # 诊断容器加两个网络位;限定在容器自己的网络命名空间内生效 # 需求三:容器内需要 setuid 程序正常工作(默认集合里有 SETUID, # 但有些加固过的镜像会主动丢弃) podman run --rm --cap-add SETUID myapp:hardened ... # 反面教材(Docker 教程里最常见的坏习惯): # podman run --privileged ... # privileged = 一次发全部钥匙 + 关掉多数防线。 # 除非在模拟硬件访问等特殊场景,它几乎总是"懒得审计"的代名词
减法比加法更常被忽视、也更该成为肌肉记忆。默认集合是"让大多数镜像能跑"的妥协值,具体到你的容器,多数位是闲置的——闲置的位就是白送的攻击面。实操示例:
# 一个纯静态网站容器,审计它实际需要什么: # 读文件(DAC 类已有)、监听高位端口(无需 NET_BIND_SERVICE)、 # 不需要 CHOWN(不落盘)、不需要 SETUID(无 su 程序) podman run --rm \ --cap-drop SETUID --cap-drop SETGID --cap-drop CHOWN \ --cap-drop NET_BIND_SERVICE --cap-drop FOWNER \ -p 8080:8080 static-site:v1 # 减掉五位后照常服务。攻击面收缩的意义: # 若镜像被植入恶意代码,它能调用的特权路径少五条 # 激进方案:all 减法 + 个别加法 podman run --rm --cap-drop ALL --cap-add NET_BIND_SERVICE \ -p 8080:80 myapp:v1 # 先清空再按需发放——审计角度最干净的写法, # 代价是镜像若有隐藏的 setuid 依赖会立刻暴露(这其实是好事: # 把隐患暴露在上线前而不是生产中)
展示一个完整排错过程,顺带理解一个高频依赖。背景:团队按"能减则减"把一个 Python 应用容器减到 --cap-drop ALL,应用启动即崩,日志只有一句 Permission denied。操作:先定位——日志没有堆栈细节,改用 strace 思路,在容器里以普通权限跑 podman run --rm --cap-drop ALL python:3.12 python -c "import socket; socket.socket().bind(('',8080))",失败;逐位试探——加回 NET_BIND_SERVICE 后 bind 成功。解读:应用配置文件里写的是 8080 端口,但它内部的 gunicorn 默认尝试先绑 80 再回退(历史遗留配置),减权限把这段"平时没人注意的回退逻辑"暴露成了硬失败。修复:修掉遗留配置,保持 cap-drop ALL。变式:很多"减权限后崩溃"的真实原因不是权限模型难懂,而是应用里藏着没人记得的历史行为——最小权限是最好的应用考古工具。
三层防线配合的典型场景:容器需要抓自己命名空间内的包。NET_RAW 位(capabilities 层)授予 raw socket 权限;seccomp 层默认放行 socket 相关调用,无需动;SELinux 层对网络操作没有文件域冲突,也无需动。三层各司其职时,改一层就够。反过来,如果加位之后仍失败,排查顺序是:先看 capabilities 位是否真的进了容器(podman exec 容器 capsh --print 可查),再看 seccomp 是否拦截了调用(报 EPERM 而位已在),最后查 SELinux 审计日志。定位工具齐全,剩下的只是习惯。