在 Docker 中运行 Web 服务时,最稳妥的防提权方案是强制容器进程以非 root 用户身份运行,配合只读根文件系统并丢弃多余的内核能力,这适用于绝大多数无特权依赖的 Web 应用。
先说结论:默认 root 运行风险极高,必须通过 Dockerfile 指定用户、运行时丢弃能力、限制挂载权限三层防护来收缩攻击面。
- 先判断:确认业务是否真的需要 root 权限,大多数 Web 服务不需要修改系统配置。
- 优先做:在镜像构建时创建专用用户并切换,启动时加上
`--cap-drop`=ALL和`--read-only`参数。 - 再验证:进入容器执行 id 命令确认 UID 非 0,尝试写入根目录确认权限被拒绝。
命令速用版
如果你需要快速启动一个安全加固的容器,可以参考以下命令模板。注意替换镜像名和挂载路径:
docker run -d \
`--name` web-safe \
`--user` 1001:1001 \
`--cap-drop`=ALL \
`--cap-add`=NET_BIND_SERVICE \
`--read-only` \
`--tmpfs` /tmp \
`--security-opt`=no-new-privileges:true \
-v /host/data:/app/data:ro \
my-web-image为什么会这样
Docker 容器默认以 root 用户启动,这意味着容器内的进程拥有 UID 0 的权限。虽然容器有命名空间隔离,但 root 权限仍然允许进程访问宿主机设备、修改系统配置或通过挂载路径逃逸。一旦 Web 应用存在漏洞(如远程代码执行),攻击者就能利用 root 权限控制容器甚至宿主机。通过降权运行,即使应用被攻破,攻击者也只能在一个受限的低权限环境中操作,无法修改关键系统文件或加载内核模块。
分步处理
1. 在 Dockerfile 中创建非 root 用户
不要依赖默认用户,显式创建并切换。建议使用固定 UID(如 1001)以避免与宿主机用户冲突:
FROM ubuntu:22.04
RUN groupadd -r appuser && useradd -r -g appuser -u 1001 appuser
COPY `--chown`=appuser:appuser ./app /app
USER 1001
CMD ["./start.sh"]2. 调整宿主机挂载目录权限
如果容器需要写入数据,宿主机目录必须归属该 UID,否则会出现权限错误:
sudo chown -R 1001:1001 /host/data3. 运行时限制能力与文件系统
启动时丢弃所有 Linux capabilities,仅添加必要的(如绑定低端口需要 NET_BIND_SERVICE)。启用根文件系统只读,防止恶意修改:
docker run `--cap-drop`=ALL `--cap-add`=NET_BIND_SERVICE `--read-only` ...4. 禁止特权模式
绝对不要使用 `--privileged` 参数,也不要挂载敏感主机路径(如 /proc、/sys、/)。
怎么验证是否生效
1. 检查运行用户
进入容器执行 id 命令,确认输出不是 uid=0(root):
docker exec -it web-safe id
# 预期输出:uid=1001(appuser) gid=1001(appuser)2. 测试文件系统只读
尝试在根目录创建文件,确认操作被拒绝:
docker exec -it web-safe touch /testfile
# 预期输出:Read-only file system3. 检查能力集
使用 capsh 或查看 proc 文件系统确认能力已被丢弃:
docker exec -it web-safe cat /proc/self/status | grep CapEff
# 预期输出:CapEff: 0000000000000000 (全零表示无能力)常见坑
1. 挂载卷权限不符
如果宿主机目录属主是 root,容器内非 root 用户无法写入。需提前 chown 或在 Compose 中指定 user 字段保持一致。
2. 应用需要写临时文件
开启 `--read-only` 后,/tmp 等目录也会只读。需使用 `--tmpfs` /tmp 或挂载独立可写卷来解决。
3. 端口绑定限制
非 root 用户默认不能绑定 1024 以下端口。如需使用 80/443,需添加 NET_BIND_SERVICE 能力或使用反向代理。