Docker 容器内运行 Web 服务如何限制权限防止提权攻击?

文章导读
在 Docker 中运行 Web 服务时,最稳妥的防提权方案是强制容器进程以非 root 用户身份运行,配合只读根文件系统并丢弃多余的内核能力,这适用于绝大多数无特权依赖的 Web 应用。
📋 目录
  1. 命令速用版
  2. 为什么会这样
  3. 分步处理
  4. 怎么验证是否生效
  5. 常见坑
  6. 参考来源
A A

在 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/data

3. 运行时限制能力与文件系统

启动时丢弃所有 Linux capabilities,仅添加必要的(如绑定低端口需要 NET_BIND_SERVICE)。启用根文件系统只读,防止恶意修改:

docker run `--cap-drop`=ALL `--cap-add`=NET_BIND_SERVICE `--read-only` ...

4. 禁止特权模式

绝对不要使用 `--privileged` 参数,也不要挂载敏感主机路径(如 /proc、/sys、/)。

怎么验证是否生效

1. 检查运行用户

Docker 容器内运行 Web 服务如何限制权限防止提权攻击?

进入容器执行 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 system

3. 检查能力集

使用 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 能力或使用反向代理。

参考来源