ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Docker未授权API逃逸:从2375端口裸奔到容器安全加固实战

Docker未授权API逃逸:从2375端口裸奔到容器安全加固实战 前阵子帮一个朋友的公司处理告警排查一台没有业务流量的测试服务器。机器 CPU 莫名其妙长时间跑满top 里看到几个陌生进程再翻 Docker 容器列表多了好几个从未部署过的镜像。顺着一查原因简单到有点讽刺这台机器上的 Docker 未授权 API 直接暴露端口 2375 裸奔在公网任何人都能连上来创建容器、执行命令。这类问题就是典型的“Docker 未授权 API 逃逸”一句话概括Docker 守护进程的 API 被未授权访问入侵者通过创建恶意容器或直接操作守护进程最终拿到宿主机上的执行能力。这文章我打算从实际运维视角聊透两件事一是这问题为什么会存在、攻击面到底在哪二是我自己的巡检、加固、排障思路。不堆概念重点放在可落地的命令、配置和踩坑经验上。1. 未授权访问链路拆解Docker API 如何变成宿主机入口1.1 Docker 的三条通信通道与最常见的裸奔方式Docker 的命令行工具本身不干活所有docker run、docker exec、docker ps最终都是通过 REST API 与守护进程 dockerd 通信。也就是说只要掌握了 API 的访问能力就等同于掌握了 Docker 的完整操作权限。守护进程默认提供三种通信通道通道类型默认位置安全性典型说明Unix Socket/var/run/docker.sock依赖文件权限本机进程通过 Socket 访问最安全TCP 2375无完全没有加密和认证最常见的不安全暴露方式TCP 2376无需配置 TLS 证书相对安全的远程访问方式裸奔的最常见原因是启动 dockerd 时带了类似-H tcp://0.0.0.0:2375的参数或者配置文件里写了hosts: [tcp://0.0.0.0:2375]同时没有开启 TLS 验证。这种操作在中文互联网的老教程里相当常见很多人从测试环境复制配置到生产环境顺手把外网映射也一起带了过去。另一个隐患来源是管理节点的统一部署脚本。有些内部工具为了简化操作在安全组或防火墙里直接放行了 2375 端口又没有做来源 IP 限制。测试连得方便了暴露面也跟着扩大。1.2 未认证调用者能获得什么一旦 2375 端口可访问攻击者不需要任何凭证就能调用 Docker API 的大部分能力读取容器列表、镜像列表、守护进程信息拉取任意镜像并运行容器在已运行容器内执行命令删除或停止任意容器修改镜像标签、导出容器文件系统这些能力放在普通运维眼里只是“方便”放在攻击者眼里就是远程指令执行。因为 dockerd 本身以 root 权限运行它创建的容器进程、处理的挂载操作、执行的命令全部继承 root 权限。调用方能不能真正控制宿主机取决于调用方提出什么样的容器创建请求。打个比方这就好比物业把整栋楼的总控制台搬到了门口而且不设门禁。任何一个路过的人都能进去按几个按钮把楼里某个房间的门拆了甚至把地下室的设备间打开。1.3 为什么未授权 API 场景下绕不开“逃逸”话题很多人会把容器逃逸理解成容器内攻击者通过内核漏洞突破隔离。但在 Docker 未授权 API 的语境下逃逸往往不需要漏洞作为前提。攻击者通过 API 创建容器时可以指定特权模式、宿主网络、挂载宿主机目录等参数。这个操作本身发生在 Docker API 层合法得不能再合法。只要守护进程接受了这个创建请求一个看似“普通”的容器实际上就获得了宿主机文件系统的访问边界。之后的“逃逸”只是顺水推舟——读一下宿主机挂载的/etc和/root写一个 SSH 公钥或者计划任务回头再通过正常通道登录宿主机。所以对运维来说必须把 Docker API 访问控制当成宿主机安全的第一道大门。门没关好后面讨论再多的 seccomp、AppArmor、Capabilities 都是徒劳。2. 自查巡检三步定位裸奔的 Docker 服务与其等人来打不如先自己查。下面的步骤不需要额外工具在目标机器上直接执行就能有个初步结论。2.1 查监听端口与守护进程启动参数第一步看端口和进程参数ss -lntp | grep -E :(2375|2376)\b ss -lx | grep docker ps -ef | grep dockerd如果ss -lntp没有任何 2375 或 2376 输出只剩一个/var/run/docker.sock的 Socket 监听那网络层风险基本排除。一旦看到tcp://0.0.0.0:2375这样的启动参数不用犹豫这就是高危信号。再顺手看一眼配置文件cat /etc/docker/daemon.json 2/dev/null systemctl cat docker.servicedaemon.json里的hosts字段是最容易藏问题的地方。有些时候命令行参数看着正常但配置文件里的hosts覆盖了默认行为结果照样开了一个 TCP 监听口。2.2 不带证书直接探测 API 响应端口开着不代表一定出问题还要确认是否真的能访问、是否真的没启用 TLS。本地探测几条命令足够curl -s http://127.0.0.1:2375/info curl -s https://127.0.0.1:2376/version curl -sk https://127.0.0.1:2376/version第一条请求如果返回类似{ID:...}的 JSON 内容说明 2375 就是一个完全开放的 API连证书都没有。对 2376 端口分别用带-k和不带-k的方式访问。如果服务端配置了 TLS不带-k会报证书错误带-k能返回版本信息说明服务端启用了 HTTPS 但客户端尚未配置证书。如果两条请求都失败端口可能被防火墙拦截或者服务并没有正常监听。2.3 排查容易被忽略的“伪安全”状态有一种情况特别容易骗过直觉Docker 配置了 TLS但证书和来源 IP 控制都没做或者只对 2375 做了 IP 白名单白名单覆盖的是整个内网网段。内网一旦被横向渗透这个口子照样瞬间被打穿。还有一些隐患藏在环境变量和登录配置里env | grep -i docker cat ~/.docker/config.json有些人会把DOCKER_HOST指向局域网内的某台 Docker 主机还习惯性关掉 TLS 验证。这种客户端配置一旦泄露等于替别人准备了访问凭证。自查时也要把这一类配置一并盘一遍毕竟攻击者经常从配置文件里找突破口。3. 容器逃逸的典型路径与最小权限防守逻辑就算把 2375 端口封住了容器安全也不能到此为止。理解逃逸路径的存在形式才能知道该在哪些层面做限制。这里我不展开具体的利用链构造方式只从防守视角讲清楚风险点和对应的控制策略。3.1 特权容器与宿主机目录挂载最直接的越界手段Docker 的隔离不是虚拟机级别的隔离。内核是同一个设备是同一批真正起作用的是命名空间、CGroup 和 Capabilities 这套组合机制。一旦创建容器时指定了特权模式就等于告诉内核这个容器不做隔离。典型的危险参数组合包括--privileged关闭绝大部分隔离限制--pidhost共享宿主进程空间--networkhost共享宿主网络栈-v /:/host把宿主机根目录挂进容器在未授权 API 场景下攻击者不需要交互式登录只需要发送一个容器创建请求把这些参数带上然后等待容器启动。对 Docker API 来说这是合理调用没有任何异常提示。映射到实际危害挂载宿主机根目录后攻击者可以直接读取宿主机/etc/shadow、数据库备份、业务配置修改/root/.ssh/authorized_keys或者往 crontab 里写入持久化任务。整个过程可以完全绕开容器逃逸漏洞的概念走的是配置层面的“合法越权”。防守逻辑很简单容器运行时参数必须最小化。不允许任何业务容器使用特权模式不允许挂载宿主机敏感目录不允许共享宿主网络和进程空间。这些限制如果只靠人肉审查早晚会漏最好通过镜像仓库策略和 Kubernetes 准入控制器一类的自动化门禁来强制。3.2 容器内挂载 Docker Socket 的连锁反应另一个典型逃生路径藏在 CI/CD 和监控工具里。很多容器化监控组件或构建流水线为了让容器内的程序能管理 Docker直接把/var/run/docker.sock挂载到容器内部。表面上这是给应用开了个“管理口子”实际上等于把宿主机钥匙交给了容器内进程。如果这个容器存在命令注入、WebShell、反序列化漏洞攻击者先拿下容器内的执行权再通过挂载进去的 Socket 调用宿主机 Docker API。后续就和未授权 2375 一样创建特权容器、挂载宿主目录、获得宿主机权限。这类问题的根源不是 Socket 本身有问题而是容器获得了一个超出业务需求的宿主机控制权。如果一个 Web 应用根本不需要调用 Docker API就不该把 Socket 挂进去。确实需要用的场景也应该通过带鉴权的代理来转发并且限定允许调用的 API 子集和来源 IP。3.3 命名空间与能力机制理解逃逸才能防御把命名空间、Capabilities、Seccomp 这三者放在一起看容器逃逸的本质就很清楚了攻击者在容器内获得了某种超出预期的高权限能力然后利用这个能力去操作容器外部的资源。举个例子普通容器默认使用非特权用户、有限 Capabilities即便进程拿到了一定权限也无法对宿主机设备进行直接读写。但如果你给容器加了CAP_SYS_ADMIN这个能力本身就可能导致挂载、命名空间操作等危险行为如果再叠加一些系统调用上的漏洞逃逸就不只是理论可能。所以防守不能只看 API 端口还要构建一个纵深体系API 层关闭未授权访问、启用 TLS、来源白名单运行层去掉特权模式、去掉多余 Capabilities内核层启用 Seccomp 和 AppArmor 默认策略业务层镜像最小化、进程以非 root 用户运行每一层都可能被绕过但每一层都增加了攻击成本。安全方案如果有一层没做问题往往就出在这一层。4. Docker daemon 加固实操从止血到完整 TLS 方案4.1 先止血立刻关闭公网监听端口发现 2375 对外监听后第一时间是止血而不是研究高级防御。把启动参数或配置文件里的 TCP 监听去掉只保留本地 Socket。修改两个地方之一即可修改/etc/docker/daemon.json{ hosts: [unix:///var/run/docker.sock] }修改 systemd 服务文件/lib/systemd/system/docker.service把启动参数里的-H tcp://0.0.0.0:2375去掉只保留ExecStart/usr/bin/dockerd -H fd:// --containerd/run/containerd/containerd.sock保存后执行systemctl daemon-reload systemctl restart docker重启前先确认本地容器存量不大、业务可接受短暂中断。如果机器上跑着数据库或核心服务先变更最小化操作或等业务窗口再重启。如果业务确实需要远程管理 Docker我的建议是优先走 SSH 隧道或者堡垒机。在跳板机上执行远程命令不直接开放 Docker 的 TCP 端口这个方案最稳。4.2 生成证书并为 Docker API 启用 TLS 验证如果远程调用确实避不开那就必须上 TLS。Docker 官方支持双向 TLS 认证既能加密通信也能识别客户端身份。我把生成证书的步骤完整列一遍。证书文件统一放在/opt/docker-certs目录mkdir -p /opt/docker-certs cd /opt/docker-certs生成 CA 私钥和 CA 证书openssl genrsa -aes256 -out ca-key.pem 4096 openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem \ -subj /CNDocker-CA生成服务端私钥和证书签名请求注意使用服务器的内网 IP 或域名openssl genrsa -out server-key.pem 4096 openssl req -subj /CN192.0.2.10 -new -key server-key.pem -out server.csr为了让客户端验证服务端地址还需要在证书里附加 Subject Alternative Name。可以创建一个扩展文件server-ext.cnfbasicConstraints CA:FALSE keyUsage digitalSignature, keyEncipherment extendedKeyUsage serverAuth subjectAltName IP:192.0.2.10,DNS:docker.local用 CA 签发服务端证书openssl x509 -req -in server.csr -CA ca.pem -CAkey ca-key.pem \ -CAcreateserial -out server-cert.pem -days 365 -extfile server-ext.cnf接着生成客户端证书给运维人员自己用openssl genrsa -out client-key.pem 4096 openssl req -subj /CNdevops-client -new -key client-key.pem -out client.csr echo extendedKeyUsage clientAuth client-ext.cnf openssl x509 -req -in client.csr -CA ca.pem -CAkey ca-key.pem \ -CAcreateserial -out client-cert.pem -days 365 -extfile client-ext.cnf然后把服务端证书配置进daemon.json{ hosts: [unix:///var/run/docker.sock, tcp://0.0.0.0:2376], tlsverify: true, tlscacert: /opt/docker-certs/ca.pem, tlscert: /opt/docker-certs/server-cert.pem, tlskey: /opt/docker-certs/server-key.pem }重启 Docker 后用客户端证书验证连接docker --tlsverify \ --tlscacert/opt/docker-certs/ca.pem \ --tlscert/opt/docker-certs/client-cert.pem \ --tlskey/opt/docker-certs/client-key.pem \ -H tcp://192.0.2.10:2376 version可以正常输出版本信息说明 TLS 生效。平时建议把客户端证书放到~/.docker/下并设置DOCKER_HOST和DOCKER_TLS_VERIFY环境变量日常命令就不用每次都带一堆证书参数。有几个经验供参考CA 私钥一定要保管好建议离线保存不要放在部署目录里随包分发证书有效期设为 365 天并写入日历提醒到期前提前轮换更换证书时先更新客户端再重启 dockerd避免中间出现连接空窗4.3 网络层访问控制白名单为主即使配了 TLS也不要让 2376 端口面向全网段开放。TLS 解决的是“通信加密和客户端身份验证”但访问范围应该收得越窄越好。云环境下的做法是在安全组里限制来源只允许运维跳板机 IP 访问 2376只允许内网管理网段访问明确拒绝公网来源传统物理机或虚拟机环境可以用 iptables 控制iptables -A INPUT -p tcp --dport 2376 -s 10.0.0.0/8 -j ACCEPT iptables -A INPUT -p tcp --dport 2376 -j DROP注意规则顺序先放行白名单再执行丢弃。如果你在云上配置了安全组同时还在机器内部写 iptables 规则要确认安全组优先级和 iptables 规则不会互相冲突。之前踩过坑安全组放行了一大段网段机器内部 iptables 又只挡了 23752376 反而漏掉了。4.4 运行时安全基线daemon.json 与容器默认策略API 层的口子堵住了还得保证容器创建时的默认配置不过分宽松。daemon.json是一个集中收敛配置的地方可以预先定一批安全驼峰{ icc: false, iptables: true, userland-proxy: false, live-restore: true, log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 } }各字段的作用icc: false关闭容器间默认通信业务需要互访时单独放行iptables: true让 Docker 管理宿主机防火墙规则同时便于流量控制live-restore: true守护进程升级或短暂重启时容器不退出日志限制避免容器日志写满磁盘容器创建时也应该有一套自己的约束规范。我建议至少做到进程以非 root 用户运行镜像里创建独立用户去掉--privileged需要设备权限时逐项添加 Capabilities文件系统尽量只读必要的目录单独挂载默认设置--security-opt no-new-privileges不把宿主机关键目录挂进容器这些限制落在代码里才有意义。写成文档叫“规范”写成 CI 检查叫“落地”。我一般是把检查逻辑直接放进部署流水线镜像构建结束后自动扫描危险配置发现问题直接拦截发布。5. 入侵后的排查、取证与常态化监控5.1 从 Docker 事件和容器列表还原现场如果机器已经疑似被入侵第一件事不是马上把容器全停掉而是先留证据、还原现场。Docker 本身自带事件流很多信息可以从里面翻出来。查看当前容器和镜像情况docker ps -a --no-trunc docker images docker top container_name -eo pid,cmd查看守护进程日志找容器创建时间、镜像拉取记录journalctl -u docker --since 7 days ago | grep -iE container|image|pull|create用 Docker 事件输出一段时间内的 API 调用docker events --since 7d --until now --filter typecontainer从事件流里通常能看到某台未知镜像的 pull 操作、异常容器的 create 时间点。再结合宿主机登录日志基本上能锁定入侵开始的时间范围。5.2 宿主层后门排查要点容器只是切入点攻击者真正的目标是宿主机持久化。检查范围至少覆盖这几类SSH 授权文件/root/.ssh/authorized_keysCrontab/var/spool/cron/和/etc/cron.d/系统服务systemctl list-units --staterunning可疑进程ps -ef与/proc目录下的异常二进制监听端口对比正常基线的陌生外联遇到过一种情况攻击者通过特权容器把宿主机根目录挂载进去写了条 crontab 来拉起挖矿进程。单看容器列表觉得“只是跑了个陌生容器”实际宿主机后门已经埋好。这类问题的处理重点是清理宿主侧而不是只删容器。清理流程我建议按这个顺序走先断开受影响机器与内网其他机器的非必要连接备份日志和容器、进程快照在隔离网络里分析确认持久化载体清理 crontab、SSH 公钥、可疑服务重建容器或重装系统修改相关账号密码与密钥对具体到已经失陷的机器最稳妥的方案是备份数据后重装系统。线上业务宁可切流量重部署也不要在不干净的底子上继续缝缝补补。5.3 常态化暴露面检查与告警防住一次不等于永远安全Docker 端口暴露问题很容易在变更后复发。我这边已经在用一套定时巡检脚本每天跑一遍思路是定期执行前面提到的自查命令把监听端口和 TLS 配置状态发到统一告警平台。巡检的核心检查点就三个2375 是否监听2376 是否监听且 TLS 是否开启/var/run/docker.sock是否被挂载进高危容器第三点检查稍微麻烦一些需要遍历所有容器的 Mountsdocker ps -q | xargs -I {} docker inspect --format {{.Name}} {{range .Mounts}}{{.Source}} {{end}} {}结果里如果出现/var/run/docker.sock和业务容器同时出现就要确认是不是必要挂载。不是必要的话尽早改配置。安全本质上是持续对抗不是一次性通关。从默认关闭 2375到配置完整 TLS再到运行层最小权限、日常巡检这套链路走完Docker 未授权 API 引发的逃逸风险才算真正压到了可控范围。尤其是中小团队没有专职安全人员更要靠制度化的巡检把这类低级风险挡在门外。我把这些经验整理出来也是希望读到的人能少踩几个我踩过的坑。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表