容器逃逸的三个前提:privileged、docker.sock 与可写挂载

容器不是沙箱

很多人把容器当成轻量虚拟机,觉得「跑在容器里就隔离了」。实际上容器只是命名空间加 cgroups 加 capabilities,共享的是同一个内核。一旦配置放开某个口子,容器里的进程就能摸到宿主机。

真正危险的配置不多,常见的就是下面三个。它们有一个共同点:在 compose 里写起来都只有一行,看起来人畜无害。

前提一:privileged: true

services:
  app:
    image: some/app
    privileged: true

这一行等于把容器变成「几乎不受限的 root」:所有 capabilities 打开、可以访问宿主设备、可以改内核参数。进了这个容器,挂载宿主根分区、写 /etc/cron.d、操作 /dev,都不需要什么高深技巧。

它常被用来「解决权限问题」——比如某个程序要访问 USB 设备或需要 raw socket。正确的替代是只加需要的那一个 capability,或者只挂载对应的设备节点。

前提二:挂载 docker.sock

    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

把 Docker 守护进程的 socket 交给容器,等于把宿主机的 root 权限交出去:容器里执行 docker run -v /:/host --privileged ...,就能以宿主机身份为所欲为。

很多 CI、监控、面板类工具需要这个 socket。更安全的做法是用只读的 socket 代理(限制可调用的 API),或者改用专门的、权限受控的接口。

前提三:可写的宿主机目录挂载

    volumes:
      - /:/host
      - /etc:/host-etc

挂载本身不是问题,问题是可写且范围过大。挂 / 或 /etc 且带写权限,容器里改一个 systemd 单元或 authorized_keys,宿主就易主了。挂 /var/run 同样危险,那里有 socket。

原则是:只挂业务需要的那一个子目录,能只读就只读(:ro),能不用 bind mount 就用命名卷。

加固怎么写进 compose

services:
  app:
    image: some/app:1.2.3
    read_only: true
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    cap_add:
      - NET_BIND_SERVICE
    tmpfs:
      - /tmp
    user: "1000:1000"

no-new-privileges 阻止 setuid 提权,cap_drop: ALL 把能力清零再按需加,只读根文件系统让写入无处落脚,非 root 用户减少逃逸后的可利用面。这几项加起来,能挡掉大部分「配置不当导致的逃逸」。

还有一层:内核

容器共享内核,意味着内核漏洞就是逃逸通道。及时打内核补丁、开启 seccomp 与 AppArmor/SELinux,比任何应用层配置都重要。多租户场景更彻底的做法是 Kata Containers、gVisor 这类强隔离运行时。

我的判断

容器逃逸的新闻总爱讲「0day」,但实际事件里,绝大多数是配置给的权限太大。反过来也成立:把 privileged、docker.sock、可写根挂载这三样去掉,剩下的攻击面会小到需要真正的内核漏洞才能突破。

所以审一份 compose 时,先搜这三个关键词,比读一百页安全文档有用。

延伸阅读

© 版权声明
THE END
喜欢就支持一下吧
点赞10 分享
评论 抢沙发
头像/SUENCA
欢迎您留下宝贵的见解!
提交
头像/SUENCA

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容