前阵子帮一个朋友的公司处理告警,排查一台没有业务流量的测试服务器。机器 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:关闭绝大部分隔离限制--pid=host:共享宿主进程空间--network=host:共享宿主网络栈-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 "/CN=Docker-CA"生成服务端私钥和证书签名请求,注意使用服务器的内网 IP 或域名:
openssl genrsa -out server-key.pem 4096 openssl req -subj "/CN=192.0.2.10" -new -key server-key.pem -out server.csr为了让客户端验证服务端地址,还需要在证书里附加 Subject Alternative Name。可以创建一个扩展文件server-ext.cnf:
basicConstraints = 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 "/CN=devops-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 又只挡了 2375,2376 反而漏掉了。
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 type=container从事件流里通常能看到某台未知镜像的 pull 操作、异常容器的 create 时间点。再结合宿主机登录日志,基本上能锁定入侵开始的时间范围。
5.2 宿主层后门排查要点
容器只是切入点,攻击者真正的目标是宿主机持久化。检查范围至少覆盖这几类:
- SSH 授权文件:
/root/.ssh/authorized_keys - Crontab:
/var/spool/cron/和/etc/cron.d/ - 系统服务:
systemctl list-units --state=running - 可疑进程:
ps -ef与/proc目录下的异常二进制 - 监听端口:对比正常基线的陌生外联
遇到过一种情况:攻击者通过特权容器把宿主机根目录挂载进去,写了条 crontab 来拉起挖矿进程。单看容器列表觉得“只是跑了个陌生容器”,实际宿主机后门已经埋好。这类问题的处理重点是清理宿主侧,而不是只删容器。
清理流程我建议按这个顺序走:
- 先断开受影响机器与内网其他机器的非必要连接
- 备份日志和容器、进程快照
- 在隔离网络里分析,确认持久化载体
- 清理 crontab、SSH 公钥、可疑服务
- 重建容器或重装系统
- 修改相关账号密码与密钥对
具体到已经失陷的机器,最稳妥的方案是备份数据后重装系统。线上业务宁可切流量重部署,也不要在不干净的底子上继续缝缝补补。
5.3 常态化暴露面检查与告警
防住一次不等于永远安全,Docker 端口暴露问题很容易在变更后复发。我这边已经在用一套定时巡检脚本,每天跑一遍,思路是定期执行前面提到的自查命令,把监听端口和 TLS 配置状态发到统一告警平台。
巡检的核心检查点就三个:
- 2375 是否监听
- 2376 是否监听且 TLS 是否开启
/var/run/docker.sock是否被挂载进高危容器
第三点检查稍微麻烦一些,需要遍历所有容器的 Mounts:
docker ps -q | xargs -I {} docker inspect --format '{{.Name}} {{range .Mounts}}{{.Source}} {{end}}' {}结果里如果出现/var/run/docker.sock和业务容器同时出现,就要确认是不是必要挂载。不是必要的话,尽早改配置。
安全本质上是持续对抗,不是一次性通关。从默认关闭 2375,到配置完整 TLS,再到运行层最小权限、日常巡检,这套链路走完,Docker 未授权 API 引发的逃逸风险才算真正压到了可控范围。尤其是中小团队,没有专职安全人员,更要靠制度化的巡检把这类低级风险挡在门外。我把这些经验整理出来,也是希望读到的人能少踩几个我踩过的坑。