老服务器上的站往容器里搬,流程一般挺顺:挑个官方镜像,COPY 代码,映射端口,起来就能访问。迁完再看进程,宿主机上原本归 www-data 的 PHP 或 Node,在容器里成了 uid 0,因为镜像没写 USER 时默认就用 root。觉得没问题的人说容器隔离更好,反对的人说里面跑 root 等于白给,两边都能讲出道理。
比的是运行身份和能碰到的东西
宿主机上用 nologin 的 www 用户跑,应用被打穿后,攻击者拿到的只是一个低权限 shell。想改系统文件、装常驻后门,他还得再找一个本地提权漏洞。这一步未必难,但终归多了道坎。
容器里的 uid 0 默认不做映射,到了宿主机上照样算 0 号用户。Docker 默认只给容器留一小撮 capability,挂载、加载内核模块这类操作都被拒掉,官方安全文档认为入侵者即便在里面拿到 root,也很难造成严重破坏或者逃到宿主。麻烦在后半截:dockerd 默认以 root 身份运行。万一攻击者借内核漏洞或配置失误逃了出来,落地就是宿主机最高权限,www 用户那道提权的坎就直接省掉了。
反过来,容器里改用普通账号跑,被攻破后的处境至少不比宿主机上的 www 用户差,外面还多套了一层命名空间。官方文档的结论也是这个意思:容器默认就相当安全,进程以非特权用户运行时尤其如此。
所以「Docker 和宿主机谁更安全」这个问法有点偏。该看的是进程用哪个 uid 运行,以及它被攻破后能摸到哪些文件、socket 和内核接口。
官方建议按这几档来
userns-remap 文档开头写明,防止容器内提权攻击,最好的办法是让应用以非特权用户运行。按改动从小到大排:
- 在 Dockerfile 里建普通用户,再用
USER切过去。最佳实践页建议显式指定 UID/GID,否则重新构建时分到的号可能会变。临时测试也可以在docker run时加--user。 - 镜像非得用 root 启动的,在 daemon 上开 userns-remap。容器里的 0 号用户会映射到
/etc/subuid分配的一段高位 UID,这些编号在宿主机上没有任何特权。dockerd 自身仍然跑在 root 下。 - 更进一步是 rootless 模式,daemon 和容器全都不用 root。按 UID/GID 映射页的说明,rootless 下容器里的 root 对应启动 Docker 的那个宿主用户。
Dockerfile 里至少要加这几行:
FROM debian:bookworm-slim
RUN groupadd -r -g 10001 app \
&& useradd --no-log-init -r -u 10001 -g app app
COPY --chown=app:app ./dist /app
USER app
CMD ["/app/server"]
userns-remap 写进 /etc/docker/daemon.json,填 default 会让 Docker 自动创建 dockremap 用户:
{
"userns-remap": "default"
}
保存后执行 sudo systemctl restart docker。这个功能有几处限制要提前知道。它不能与宿主机共享 PID 或网络命名空间,--privileged 必须搭配 --userns=host 才能用。挂载宿主机目录时,文件属主得按映射后的 UID 提前调好,不然容器里写不进去。开启后 /var/lib/docker 里原有的镜像和容器会被屏蔽,文档建议在新装的 Docker 上启用。
rootless 想监听 1024 以下端口,需要给 rootlesskit 设置 cap_net_bind_service 或者调整 sysctl,前面套一层宿主机 Nginx 反代通常更省事。装好后运行 docker info,Security Options 里出现 rootless 字样,说明客户端连上的正是这个无 root 的 daemon。
不管选哪档,这几处顺手收紧
- 业务容器别挂
/var/run/docker.sock。docker run 参考里写着,挂进去就等于让容器完全掌控宿主机上的 Docker daemon。 - 宿主机上的 www 用户别加进 docker 组,安装后配置页明确警告这个组会授予 root 级别的权限。
- 根文件系统设为只读,需要写入的目录单独挂 tmpfs 或卷。
- capability 先全部去掉,缺什么再补上,安全文档给的建议也是只留进程明确需要的。
docker run -d --name web \
--user 10001:10001 \
--read-only --tmpfs /tmp \
--cap-drop=ALL \
-p 8080:8080 myapp:latest
容器起来后执行 docker exec web id,看到的实际 uid 比 Dockerfile 里写的更可信。
站里那篇讲 PHP 老站被打 的帖子说的是怎么堵洞。这篇说的是另一头:洞没堵上、站被打穿以后,运行身份决定了损失有多大。两件事可以一起做。
你们线上的容器里,真正写了 USER 的占几成?